Mythic3.3.0 Initial push

This commit is contained in:
its-a-feature
2024-07-09 17:34:09 -05:00
parent 2079bd7ff0
commit e6c4792151
380 changed files with 32516 additions and 17574 deletions
+5 -3
View File
@@ -9,7 +9,7 @@ on:
# types: [published]
push:
branches:
- 'docker_updates'
- 'Mythic3.3'
- 'master'
tags:
- "v*.*.*"
@@ -43,8 +43,8 @@ env:
# Set the container image version to the Github release tag
VERSION: ${{ github.ref_name }}
# Branch for pushing release changes (TODO: Change this to the main branch when the rewrite is finished)
RELEASE_BRANCH: master
# Branch for pushing release changes
RELEASE_BRANCH: Mythic3.3
jobs:
# Builds the base container image and pushes it to the container registry
@@ -72,6 +72,8 @@ jobs:
# the following are unique to this job
- name: Lowercase the server container image name
run: echo "MYTHIC_SERVER_IMAGE_NAME=${MYTHIC_SERVER_IMAGE_NAME,,}" >> ${GITHUB_ENV}
- name: Copy version file over
run: cp VERSION mythic-docker/src/VERSION
- name: Build and push the server container image
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
with:
+102
View File
@@ -4,6 +4,108 @@ 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.0-rc1] - 2024-07-09
### Changed
- Fixed how external_ip is fetched from containers to provide a more accurate representation
- help and clear appear in generic `help` output
- ability to hide callbacks that use the PushOneToMany
- updated Mythic's cookie to use strict same site and http only flags
- tasking input - fixed issue where double options could be presented when using tab
- tasking input - adjusted so complex types (link info, files, payloads, etc) aren't tab-completable
- This reduces some confusion when tab completing command parameters
- expand/hide subtasks in UI that have subtasks
- moved plaintext output expand icon to the left of 1 in text editor instead of in the middle
- fixed an issue where sometimes if a tab was open, clicking the keyboard for a callback wouldn't bring that tab back to focus
- fetch server version dynamically in the UI so it updates more often
- update mythic-cli allowed_ips to apply to all web/scripting routes, not just auth
- this now applies to all routes and sub-routes behind Mythic's local reverse proxy
- cache hasura information and invalidate / re-fetch after any modification to operator operation status
- each request went from 3-10ms to 700-1000micro seconds in processing time
- updated list/edit/delete/upload file features for containers to _all_ containers instead of just C2 containers
- fixed bug where non-utf8 characters in keylog data would error on the page
- function to get graphql schema (or option from Mythic)
- `schema = await mythic.mythic_utilities.fetch_graphql_schema(mythic=mythic_instance)`
- This is helpful when trying to do GraphQL via Golang
- only admins can create new operators
- only admins can create new operations
- fix the UI when the width is too small causing the top appbar to take up 2 lines and cover buttons
- now width <1100px will hide some buttons along the top
- after getting logged out, should redirect to where you were via redirect= URL parameter
- updated payloadtype definition to allow specification of UUID length pre-pended to agent messages
- This can be either 16 or 36 and defaults to 36 (the length of the normal UUIDv4 string)
- This makes it possible to have 16 Byte UUIDs used for P2P comms
- updated wrapper builds to not send the wrapped payload bytes via rabbitmq
- HTTP request to mythic made to fetch bytes from container before passing execution off to build function
- no change needed by agent developers
- jupyter access token changed from default 'mythic' to randomized 30 char password
- only affects new installs, but you'll need to fetch this value similar to fetching the hasura secret for GraphQL access
- fixed bug where zipped and downloaded files wouldn't record the final zip size or md5/sha1 hashes
- added button on keylogs page to view all keylogs within a user/host/window combination in your current search window at once
- When sending data back for the file browser, `success` is now an optional boolean field
- C2 Profile debug output is now also sent to the container's debug output so you don't have to view it through the UI
### Added
- Two new fields in agent message for artifacts for `needs_cleanup` and `resolved`
- added `process_short_name` field to Callbacks
- this is automatically parsed based on the `process_name` returned from agents when they checkin or update their callback information
- the `process_short_name` is displayed in the Mythic UI callbacks table, but the full `process_name` is shown in the callback detailed metadata view
- this allows agents to return the full path to the binary when checking in without worrying about it bloating up the UI
- light and dark mode agent icon support
- If no dark mode icon is provided, the light mode version is used for both by default on new sync
- There's a new field on payloadtype definitions for a dark mode icon
- MythicRPC call to expose Mythic's way of parsing paths so that agents don't have to do it themselves and it can be standardized
- MythicRPCFileBrowserParsePath
- added task display_id to tasks shown when doing browser script edits so that it's easier to tell the difference
- added an "email" field on operators
- add new ChooseOneCustom parameter type (build, command, and c2) to allow users to choose from list or add new value
- add new FileMultiple parameter type (build, command, and c2) to allow users to select and upload multiple files at once
- new "Last Updated" time in proxy table so you know when data is flowing
- the amount of data transfer updates every 20s
- auto-tag files as you download/preview them so that it's easier to see what has been triaged or not by the team
- all consuming containers now are tracked in the UI specifically and have their own name and description fields that must be set
- This applies to webhooks, loggers, eventing, auth
- A new type of user, a bot account, is now available for creation
- only admins can create new operator accounts and new bot accounts
- bot accounts are not available to login
- a bot account is automatically created for every operation
- bot accounts can be used to take actions in eventing (as long as the operation lead approves it)
- admins are able to generate/view/delete apitokens for bot accounts as well
- Added new `logging.UpdateLogToFile` and `logging.UpdateLogToStdout` functions to containers
- These allow you to dynamically update logging to write to file+stdout or just stdout as needed
- `mythic_container.logging.update_log_to_file` and `mythic_container.logging.update_log_to_stdout` in Python
- Admins can generate one-time-use invite codes to invite somebody to their Mythic server without pre-creating an account
- This is disabled by default but can be enabled in .env or in global settings by admins (MYTHIC_SERVER_ALLOW_INVITE_LINKS)
- Each invite link can be used only once and un-used invite links can be deleted
- Invite links become invalid when the server restarts
- SSO Support via "auth" containers
- can redirect to SSO providers (ExampleContainers has example for ADFS) that provide IDP services
- can process non SSO custom auth as well
- each case must return an email associated with a user that's logged in
- Operators now have email addresses optionally associated with them
- these can be seen via ConsumingServices page
- All containers have an `on_start` function that gets called when the container starts up
- This function is executed once for every operation that's currently running (not deleted and not complete)
- This function gets access to a special JWT APIToken that's scoped to the bot account assigned to the operation
- This JWT is for spectator access (no changes can be made) and only lasts for 5 minutes
- The goal here is to allow some basic configuration to be performed by the container
- New PayloadType attribute `agent_type` value of `command_augment`
- CommandAugment containers expose custom tasks to other PayloadTypes and are automatically injected into callbacks
- Payload type definitions have a new `CheckIfCallbacksAliveFunction`
- This gets a list of active callbacks based on this payload type along with their id, last checkin, first checkin, and sleep_info information
- This returns back a list of all the callbacks and an indication if they should be marked as "dead" or not
- "dead" status is reflected by a red skull in the last_checkin column in the callbacks table
- the `sleep_info` data can be updated at any time as a free-form string via MythicRPC or the UI
- the `sleep_info` data is also a column you can toggle to view or not in the UI in the callbacks table
- Added `SendMythicRPCCallbackNextCheckinRange` RPC call to get basic range for next checkin options based on:
- last_checkin, jitter percentage, and sleep interval
- This is provided as a helpful way to reduce duplicated efforts in all payload types checking if `time.Now().UTC()` within the possible range
- New Container Type and feature: Eventing
- Eventing button at the top now added to manage eventing workflows
- New docs around eventing added
## [3.2.20-rc11] - 2024-04-23
### Changed
+4765 -2775
View File
File diff suppressed because it is too large Load Diff
+29 -29
View File
@@ -9,54 +9,54 @@
"react-scripts": "^5.0.1"
},
"dependencies": {
"@apollo/client": "^3.8.6",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-brands-svg-icons": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@mui/icons-material": "^5.14.16",
"@apollo/client": "^3.10.5",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@mui/icons-material": "^5.15.20",
"@mui/lab": "^5.0.0-alpha.138",
"@mui/material": "^5.14.16",
"@mui/x-charts": "^6.18.7",
"@mui/x-data-grid": "^6.18.6",
"@mui/x-date-pickers": "^6.17.0",
"ace-builds": "^1.31.1",
"@mui/material": "^5.15.20",
"@mui/x-charts": "^6.19.8",
"@mui/x-data-grid": "^6.20.1",
"@mui/x-date-pickers": "^6.20.1",
"ace-builds": "^1.35.0",
"anser": "^2.1.1",
"dayjs": "^1.11.10",
"dayjs": "^1.11.11",
"elkjs": "^0.8.2",
"graphql": "^16.8.1",
"graphql-ws": "^5.14.2",
"graphql": "^16.8.2",
"graphql-ws": "^5.16.0",
"html-to-image": "^1.11.11",
"html2canvas": "^1.4.1",
"ip6": "^0.2.10",
"json5": "^2.2.3",
"jwt-decode": "^3.1.2",
"material-react-table": "^2.9.2",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
"material-react-table": "^2.13.0",
"moment": "^2.30.1",
"moment-timezone": "^0.5.45",
"randexp": "^0.5.3",
"react": "^18.2.0",
"react": "^18.3.1",
"react-ace": "^10.1.0",
"react-colorful": "^5.6.1",
"react-dom": "^18.2.0",
"react-dom": "^18.3.1",
"react-draggable": "^4.4.6",
"react-moment": "^1.1.3",
"react-router-dom": "^6.18.0",
"react-router-dom": "^6.23.1",
"react-scrollbar-size": "^5.0.0",
"react-split": "^2.0.14",
"react-toastify": "^9.1.3",
"react-virtualized": "^9.22.5",
"react-virtualized-auto-sizer": "^1.0.20",
"react-window": "^1.8.9",
"reactflow": "^11.9.4",
"react-virtualized-auto-sizer": "^1.0.24",
"react-window": "^1.8.10",
"reactflow": "^11.11.3",
"save-svg-as-png": "^1.4.17",
"semver": "^7.5.4",
"styled-components": "^6.1.0",
"tough-cookie": "^4.1.3",
"semver": "^7.6.2",
"styled-components": "^6.1.11",
"tough-cookie": "^4.1.4",
"uuid": "^9.0.1",
"webpack": "^5.89.0",
"webpack": "^5.92.0",
"word-wrap": "^1.2.5"
},
"scripts": {
Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

+6
View File
@@ -30,6 +30,12 @@ export const FailedRefresh = () =>{
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
localStorage.removeItem("user");
// retrieve all cookies
let Cookies = document.cookie.split(';');
// set past expiry to all cookies
for (let i = 0; i < Cookies.length; i++) {
document.cookie = Cookies[i] + "=; expires="+ new Date(0).toUTCString();
}
meState({
loggedIn: false,
access_token: null,
+13 -7
View File
@@ -33,6 +33,8 @@ import { RefreshTokenDialog } from './RefreshTokenDialog';
import { MythicDialog } from './MythicComponents/MythicDialog';
import { ToastContainer } from 'react-toastify';
import "react-toastify/dist/ReactToastify.css";
import {Eventing} from "./pages/Eventing/Eventing";
import {InviteForm} from "./pages/Login/InviteForm";
@@ -61,7 +63,7 @@ export function App(props) {
createTheme(adaptV4Theme({
palette: {
primary: {
main: themeMode === "dark" ? "#465a79" : "rgb(102,121,145)",
main: themeMode === "dark" ? "rgb(70,91,115)" : "rgb(117,133,155)",
},
error: {
main: themeMode === "dark" ? '#da3237' : '#c42c32',
@@ -77,8 +79,10 @@ export function App(props) {
},
mode: themeMode,
background: {
contrast: themeMode === 'dark' ? '#ffffff' : '#30455e',
default: themeMode === "dark" ? 'rgb(31, 31, 31)' : '#ffffff',
contrast: themeMode === 'dark' ? '#e1e0e0' : 'rgb(44, 52, 60)',
default: themeMode === "dark" ? 'rgb(48, 48, 48)' : '#f5f5f5',
paper: themeMode === "dark" ? 'rgb(37,36,36)' : '#ececec',
taskLabel: themeMode === "dark" ? 'rgb(20, 20, 20)' : '#f5f5f5',
},
listSubHeader: {
default: themeMode === "dark" ? 'rgb(50, 50, 50)' : 'rgb(240, 240, 240)',
@@ -94,17 +98,17 @@ export function App(props) {
graphGroupRGBA: themeMode === 'dark' ? 'rgba(57, 76, 93, 0.5)' : 'rgba(211, 215, 232, 0.5)',
speedDialAction: themeMode === 'dark' ? '#495054' : '#ffffff',
},
pageHeaderColor: "white",
pageHeaderTextColor: "#ffffff",
folderColor: '#f1d592',
tableHeader: '#484848',
tableHeader: themeMode === 'dark' ? '#484848' : '#e0e0e0',
successOnMain: '#1ae302',
errorOnMain: '#ff656b',
infoOnMain: '#67ceff',
materialReactTableHeader: themeMode === 'dark' ? '#484848' : '#d5d5d5',
tableBorder: themeMode === 'dark' ? 'rgba(81,81,81,1)' : 'rgba(224,224,224,1)',
tableHover: themeMode === 'dark' ? 'rgba(85,88,93)' : 'rgba(245, 245, 245)',
tableHover: themeMode === 'dark' ? 'rgba(60,60,60)' : 'rgb(232,232,232)',
pageHeader: {
main: '#827E80',
main: '#706c6e',
},
pageHeaderSecondary: {
main: '#444343',
@@ -161,6 +165,7 @@ export function App(props) {
<div style={{ margin: '0px 5px 5px 5px', flexGrow: 1, flexDirection: 'column', height: "calc(100% - 4rem)", }}>
<Routes>
<Route path='/new/login' element={<LoginForm me={me}/>}/>
<Route path='/new/invite' element={<InviteForm me={me}/>}/>
<Route path='/' element={<LoggedInRoute me={me}><Home me={me}/></LoggedInRoute>} />
<Route exact path='/new' element={<LoggedInRoute me={me}><Home me={me}/></LoggedInRoute>} />
<Route exact path='/new/settings' element={<LoggedInRoute me={me}><Settings me={me}/></LoggedInRoute>} />
@@ -182,6 +187,7 @@ export function App(props) {
<Route exact path='/new/mitre' element={<LoggedInRoute me={me}><MitreAttack me={me}/></LoggedInRoute>} />
<Route exact path='/new/tagtypes' element={<LoggedInRoute me={me}><Tags me={me}/></LoggedInRoute>} />
<Route exact path='/new/consuming_services' element={<LoggedInRoute me={me}><ConsumingServices me={me}/></LoggedInRoute>} />
<Route exact path='/new/eventing' element={<LoggedInRoute me={me}><Eventing me={me}/></LoggedInRoute>} />
</Routes>
</div>
</div>
@@ -0,0 +1,9 @@
import {useTheme} from '@mui/material/styles';
export const MythicAgentSVGIcon = ({payload_type, style}) => {
const theme = useTheme();
return (
<img src={"/static/" + payload_type + "_" + theme.palette.mode + ".svg"}
style={{...style}}/>
)
}
@@ -13,6 +13,12 @@ import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import { toLocalTime } from '../utilities/Time';
import AceEditor from 'react-ace';
import "ace-builds/src-noconflict/mode-json";
import "ace-builds/src-noconflict/theme-github";
import "ace-builds/src-noconflict/theme-monokai";
import "ace-builds/src-noconflict/ext-searchbox";
import {useTheme} from '@mui/material/styles';
export function MythicDialog(props) {
@@ -50,21 +56,40 @@ export function MythicDialog(props) {
export function MythicModifyStringDialog(props) {
const [comment, setComment] = React.useState("");
const theme = useTheme();
const onCommitSubmit = () => {
props.onSubmit(comment);
props.onClose();
}
const onChange = (name, value, error) => {
const onChange = (value) => {
setComment(value);
}
useEffect( () => {
setComment(props.value);
try{
setComment(JSON.stringify(JSON.parse(props.value), null, 2));
}catch(error){
setComment(props.value);
}
}, [props.value]);
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">{props.title}</DialogTitle>
<DialogContent dividers={true}>
<MythicTextField autoFocus onEnter={props?.onEnter || onCommitSubmit} onChange={onChange} value={comment} multiline={props?.multiline || false} maxRows={props.maxRows} />
<DialogContent dividers={true} style={{height: "100%"}}>
<AceEditor
mode="json"
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
width="100%"
height="100%"
minLines={props.maxRows ? props.maxRows : 10}
maxLines={props.maxRows ? props.maxRows : 10}
value={comment}
focus={true}
onChange={onChange}
setOptions={{
useWorker: false
}}
/>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
@@ -146,7 +171,7 @@ export function MythicViewJSONAsTableDialog(props) {
return (
<React.Fragment>
<DialogTitle id="form-dialog-title" style={{wordBreak: "break-all", maxWidth: "100%"}}>{props.title}</DialogTitle>
<Paper elevation={5} style={{position: "relative"}} variant={"elevation"}>
<TableContainer className="mythicElement">
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
<TableHead>
@@ -212,7 +237,6 @@ export function MythicViewJSONAsTableDialog(props) {
</TableBody>
</Table>
</TableContainer>
</Paper>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
Close
@@ -79,7 +79,7 @@ return (
Send a feedback report to the slack webhook configured for the current operation.
This provides a way to easily capture a bug, feedback requests, or comments without breaking operator flow too much. <br/>
Shift+Enter will auto-submit the form.
<TableContainer component={Paper} className="mythicElement">
<TableContainer className="mythicElement">
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
<TableBody style={{whiteSpace: "pre"}}>
<TableRow hover >
@@ -233,7 +233,7 @@ const VirtualTreeRow = ({
transformOrigin: placement === 'bottom' ? 'left top' : 'left bottom',
}}
>
<Paper variant="outlined" className={"dropdownMenuColored"}>
<Paper className={"dropdownMenuColored"}>
<ClickAwayListener onClickAway={handleClose}>
<MenuList id="split-button-menu" style={{paddingTop: 0}} >
<ListSubheader component={"li"} className={"MuiListSubheader-root"}>
@@ -1,24 +1,28 @@
import { snackActions } from '../utilities/Snackbar';
import {snackActions} from '../utilities/Snackbar';
export const UploadTaskFile = async (file, comment) => {
let formData = new FormData();
formData.append("file", file);
formData.append("comment", comment);
snackActions.info("Uploading " + file.name + " to Mythic...", {autoHideDuration: 1000});
try{
formData.append("file", file);
formData.append("comment", comment);
snackActions.info("Uploading " + file.name + " to Mythic...", {autoHideDuration: 1000});
}catch(error){
console.log(error)
return null;
}
try{
const upload_response = await fetch('/api/v1.4/task_upload_file_webhook', {
method: 'POST',
body: formData,
headers: {
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
"Authorization": `Bearer ${localStorage.getItem("access_token")}`,
MythicSource: "web",
}
});
try{
const upload_result = upload_response.json().then(data => {
//console.log(data);
return data?.agent_file_id || data?.error || null;
}).catch(error => {
console.log(upload_response);
snackActions.warning("Error: " + upload_response.statusText + "\nError Code: " + upload_response.status);
console.log("Error trying to get json response", error.toString());
return null;
@@ -32,4 +36,68 @@ export const UploadTaskFile = async (file, comment) => {
snackActions.error(error.toString());
return null;
}
}
export const UploadEventFile = async (file, comment) => {
let formData = new FormData();
formData.append("file", file);
formData.append("comment", comment);
snackActions.info("Uploading " + file.name + " to Mythic...", {autoHideDuration: 1000});
try{
const upload_response = await fetch('/api/v1.4/eventing_import_webhook', {
method: 'POST',
body: formData,
headers: {
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
}
});
try{
return upload_response.json().then(data => {
//console.log(data);
return data;
}).catch(error => {
console.log(upload_response);
snackActions.warning("Error: " + upload_response.statusText + "\nError Code: " + upload_response.status);
console.log("Error trying to get json response", error.toString());
return null;
});
}catch(error){
snackActions.error(error.toString());
return null;
}
}catch(error){
snackActions.error(error.toString());
return null;
}
}
export const UploadEventGroupFile = async (file, eventgroup_id) => {
let formData = new FormData();
formData.append("eventgroup_id", eventgroup_id);
formData.append("file", file);
snackActions.info("Uploading " + file.name + " to Mythic...", {autoHideDuration: 1000});
try{
const upload_response = await fetch('/api/v1.4/eventing_register_file_webhook', {
method: 'POST',
body: formData,
headers: {
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
}
});
try{
return upload_response.json().then(data => {
//console.log(data);
return data;
}).catch(error => {
console.log(upload_response);
snackActions.warning("Error: " + upload_response.statusText + "\nError Code: " + upload_response.status);
console.log("Error trying to get json response", error.toString());
return null;
});
}catch(error){
snackActions.error(error.toString());
return null;
}
}catch(error){
snackActions.error(error.toString());
return null;
}
}
@@ -81,7 +81,7 @@ const HeaderCell = ({
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
}}
>
<Paper variant="outlined" style={{backgroundColor: theme.palette.mode === 'dark' ? theme.palette.primary.dark : theme.palette.primary.light, color: "white"}}>
<Paper variant="outlined" style={{backgroundColor: theme.palette.mode === 'dark' ? theme.palette.primary.dark : theme.palette.primary.light}}>
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
<MenuList id="split-button-menu" >
{contextMenuOptions.map((option, index) => (
@@ -9,11 +9,13 @@ setting_name options:
showHostname
showCallbackGroups
showMedia
callbacks_table_columns
callbacks_table_filters
*/
export function useMythicSetting({setting_name, default_value, output="boolean"}){
const me = useReactiveVar(meState);
// get the initial value we have stored
const localStorageSetting = localStorage.getItem(`${me?.user?.user_id || 0}-${setting_name}`);
let initialStorageSetting = localStorageSetting === null ? default_value : localStorageSetting;
switch(output){
@@ -23,16 +25,29 @@ export function useMythicSetting({setting_name, default_value, output="boolean"}
case "number":
initialStorageSetting = Number(initialStorageSetting);
break;
case "json-array":
try{
initialStorageSetting = JSON.parse(initialStorageSetting);
}catch(error){
console.log(initialStorageSetting);
}
break;
case "json":
try{
initialStorageSetting = JSON.parse(initialStorageSetting);
}catch(error){
console.log(initialStorageSetting);
}
break;
default:
console.log("unknown output type", output);
}
const [setting, setSetting] = React.useState(initialStorageSetting);
React.useEffect( () => {
// update the initial value if the user changes
const localStorageSetting = localStorage.getItem(`${me?.user?.user_id || 0}-${setting_name}`);
let initialStorageSetting = localStorageSetting === null ? default_value : localStorageSetting;
switch(output){
case "boolean":
initialStorageSetting = (initialStorageSetting.toLowerCase() === "true");
@@ -40,11 +55,38 @@ export function useMythicSetting({setting_name, default_value, output="boolean"}
case "number":
initialStorageSetting = Number(initialStorageSetting);
break;
case "json-array":
try{
initialStorageSetting = JSON.parse(initialStorageSetting);
}catch(error){
initialStorageSetting = [];
}
break;
case "json":
try{
initialStorageSetting = JSON.parse(initialStorageSetting);
}catch(error){
initialStorageSetting = {};
}
break;
default:
console.log("unknown output type", output);
}
setSetting(initialStorageSetting);
}, [me]);
return setting;
}
export function SetMythicSetting({setting_name, value, output = "boolean"}) {
let newSetting = value;
switch(output){
case "json-array":
newSetting = JSON.stringify(value);
break;
case "json":
newSetting = JSON.stringify(value);
break;
default:
}
const meID = meState()?.user?.user_id || 0;
localStorage.setItem(`${meID}-${setting_name}`, newSetting)
}
@@ -57,7 +57,7 @@ export function MythicSelectFromListDialog(props) {
<em>None</em>
</MenuItem>
{options.map( (opt) => (
<MenuItem value={opt} key={opt[props.identifier]}>{opt[props.display]}</MenuItem>
<MenuItem value={opt} key={opt[props.identifier]}>{opt?.[props.display]}</MenuItem>
) )}
</Select>
</FormControl>
@@ -15,7 +15,8 @@ export function MythicStyledTooltip(props){
fontSize: 13,
'& .MuiTooltip-arrow': {
color: theme.palette.background.contrast
}
},
zIndex: 1000
}
}
}} style={{zIndex:1000}} {...other}>
@@ -150,7 +150,7 @@ export function MythicTabLabel(props) {
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
}}
>
<Paper variant="outlined" style={{backgroundColor: theme.palette.mode === 'dark' ? theme.palette.primary.dark : theme.palette.primary.light, color: "white"}}>
<Paper variant="outlined" className={"dropdownMenuColored"}>
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
<MenuList id="split-button-menu" >
{contextMenuOptions.map((option, index) => (
@@ -46,7 +46,7 @@ const updateTagMutationTemplate = gql`
const getObjectTagsQueryTemplate = ({target_object}) => {
return gql`
query getObjectTags ($${target_object}: Int!) {
tag(where: {${target_object}: {_eq: $${target_object}}}) {
tag(where: {${target_object}: {_eq: $${target_object}}}, order_by: {tagtype: {name: asc}}) {
source
url
id
@@ -63,7 +63,7 @@ query getObjectTags ($${target_object}: Int!) {
}
const getTagtypesQuery = gql`
query getTagtype {
tagtype {
tagtype(order_by: {name: asc}) {
name
color
description
@@ -151,7 +151,7 @@ const StringTagDataEntry = ({name, value}) => {
console.log("There was an error!", error);
})
}
if(RegExp(regex).test(value)){
if(RegExp(regex)?.test(value)){
const capturePieces = RegExp(captureRegex).exec(value);
const targetPieces = RegExp(targetRegex).exec(capturePieces[3]);
const colorPieces = RegExp(colorRegex).exec(capturePieces[3]);
@@ -192,16 +192,22 @@ function ViewTagDialog(props) {
let newTag = {...data.tag_by_pk};
let tagData = newTag;
try{
tagData = JSON.parse(newTag.data);
newTag.data = tagData;
newTag.is_json = true;
if(newTag.data.constructor === Object){
newTag.data = {...data.tag_by_pk.data};
newTag.is_json = true;
} else if(typeof newTag.data === "string"){
tagData = JSON.parse(newTag.data);
newTag.data = tagData;
newTag.is_json = true;
}
}catch(error){
newTag.is_json = false;
}
setSelectedTag(newTag);
},
onError: error => {
console.log(error);
console.log("query error", error);
},
fetchPolicy: "network-only"
})
@@ -244,7 +250,7 @@ return (
<TableRow>
<TableCell>Data</TableCell>
<TableCell>
{selectedTag?.is_json || false ? (
{selectedTag?.is_json ? (
<TableContainer className="mythicElement">
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
<TableBody>
@@ -318,7 +324,12 @@ export function ViewEditTagsDialog(props) {
setSelectedTag(data.tag[0]);
setNewSource(data.tag[0].source);
setNewURL(data.tag[0].url);
setNewData(data.tag[0].data);
try{
setNewData(JSON.stringify(data.tag[0].data, null, 2));
}catch(error){
setNewData(String(data.tag[0].data));
}
}
},
onError: error => {
@@ -374,9 +385,13 @@ const onEditorChange = (value, event) => {
}
const handleTaskTypeChange = (evt) => {
setSelectedTag(evt.target.value);
setNewData(evt.target.value.data);
setNewSource(evt.target.value.source);
setNewURL(evt.target.value.url);
try{
setNewData(JSON.stringify(evt.target.value.data, null, 2));
}catch(error){
setNewData(String(evt.target.value.data));
}
}
const handleNewTagCreate = ({tagtype_id, source, url, data, id}) => {
props.onClose();
@@ -44,6 +44,7 @@ class MythicTextField extends React.Component {
variant: PropTypes.string,
inline: PropTypes.bool,
marginBottom: PropTypes.string,
value: PropTypes.any,
}
onChange = evt => {
const name = this.props.name;
@@ -77,12 +78,13 @@ class MythicTextField extends React.Component {
placeholder={this.props.placeholder}
value={this.props.value}
onChange={this.onChange}
color={"secondary"}
onKeyDown={this.onKeyPress}
label={this.props.showLabel === undefined ? this.props.name : this.props.showLabel ? this.props.name : undefined}
autoFocus={this.props.autoFocus}
variant={this.props.variant === undefined ? "outlined" : this.props.variant}
data-lpignore={true}
autoComplete={this.props.autoComplete === undefined ? "off" : (this.props.autoComplete ? "on" : "off")}
autoComplete={this.props.autoComplete === undefined ? "new-password" : (this.props.autoComplete ? "on" : "new-password")}
disabled={this.props.disabled === undefined ? false : this.props.disabled}
required={this.props.requiredValue ? this.props.requiredValue : false}
InputLabelProps={this.props.inputLabelProps}
@@ -95,6 +97,7 @@ class MythicTextField extends React.Component {
helperText={this.checkError() ? this.props.errorText : this.props.helperText}
style={{
padding:0,
paddingLeft: "5px",
marginBottom: this.props.marginBottom ? this.props.marginBottom : "10px",
display: this.props.inline ? "inline-block": "",
}}
+321 -255
View File
@@ -7,7 +7,7 @@ import Toolbar from '@mui/material/Toolbar';
import IconButton from '@mui/material/IconButton';
import MenuIcon from '@mui/icons-material/Menu';
import MenuItem from '@mui/material/MenuItem';
import ManageAccountsIcon from '@mui/icons-material/ManageAccounts';
import ManageAccountsTwoToneIcon from '@mui/icons-material/ManageAccountsTwoTone';
import Menu from '@mui/material/Menu';
import { Link } from 'react-router-dom';
import Drawer from '@mui/material/Drawer';
@@ -20,9 +20,9 @@ import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import { useReactiveVar } from '@apollo/client';
import { menuOpen, FailedRefresh } from '../cache';
import { TopAppBarNotifications } from './TopAppBarNotifications';
import { TopAppBarEventLogNotifications } from './TopAppBarEventLogNotifications';
import { EventFeedNotifications } from './EventFeedNotifications';
import HelpIcon from '@mui/icons-material/Help';
import HelpTwoToneIcon from '@mui/icons-material/HelpTwoTone';
import PhoneCallbackIcon from '@mui/icons-material/PhoneCallback';
import {ReactComponent as ReactLogo} from './mythic_red_small.svg';
import SpaceDashboardTwoToneIcon from '@mui/icons-material/SpaceDashboardTwoTone';
@@ -46,7 +46,7 @@ import AttachmentIcon from '@mui/icons-material/Attachment';
import FingerprintIcon from '@mui/icons-material/Fingerprint';
import {faSocks} from '@fortawesome/free-solid-svg-icons';
import CameraAltTwoToneIcon from '@mui/icons-material/CameraAltTwoTone';
import {mythicVersion, mythicUIVersion} from '../index';
import {mythicUIVersion} from '../index';
import {MythicStyledTooltip} from './MythicComponents/MythicStyledTooltip';
import VpnKeyIcon from '@mui/icons-material/VpnKey';
import ThumbDownTwoTone from '@mui/icons-material/ThumbDownTwoTone';
@@ -57,6 +57,10 @@ import StorageIcon from '@mui/icons-material/Storage';
import PublicIcon from '@mui/icons-material/Public';
import LightModeTwoToneIcon from '@mui/icons-material/LightModeTwoTone';
import DarkModeTwoToneIcon from '@mui/icons-material/DarkModeTwoTone';
import PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwoTone';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import {TopAppBarEventingNotifications} from "./TopAppBarEventingNotifications";
import {useQuery, gql} from '@apollo/client';
const PREFIX = 'TopAppBar';
@@ -72,7 +76,8 @@ const classes = {
appBar: `${PREFIX}-appBar`,
appBarShift: `${PREFIX}-appBarShift`,
nested: `${PREFIX}-nested`,
mythicElement: `${PREFIX}-mythicElement`
mythicElement: `${PREFIX}-mythicElement`,
menuButton: `${PREFIX}-menuButton`,
};
const StyledAppBar = styled(AppBar)((
@@ -83,6 +88,9 @@ const StyledAppBar = styled(AppBar)((
[`& .${classes.hide}`]: {
display: 'none',
},
[`& .${classes.menuButton}`]: {
display: 'inline-block',
},
[`&.${classes.appBar}`]: {
width: "100%",
backgroundColor: theme.topAppBarColor,
@@ -90,6 +98,11 @@ const StyledAppBar = styled(AppBar)((
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
borderTopRightRadius: theme.palette.mode === "dark" ? "3px" : 0,
borderTopLeftRadius: theme.palette.mode === "dark" ? "3px" : 0,
position: "sticky",
overflow: "hidden",
maxHeight: "48px"
},
[`&.${classes.appBarShift}`]: {
maxWidth: `calc(100% - ${drawerWidth}px)`,
@@ -98,6 +111,8 @@ const StyledAppBar = styled(AppBar)((
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
borderTopRightRadius: theme.palette.mode === "dark" ? "3px" : 0,
borderTopLeftRadius: theme.palette.mode === "dark" ? "3px" : 0,
},
}));
const StyledDrawer = styled(Drawer)((
@@ -126,7 +141,9 @@ const StyledDrawer = styled(Drawer)((
},
[`& .${classes.listSubHeader}`]: {
backgroundColor: theme.palette.listSubHeader.default
backgroundColor: theme.palette.listSubHeader.default,
borderTopLeftRadius: "3px",
borderTopRightRadius: "3px",
},
[`& .${classes.nested}`]: {
@@ -136,22 +153,29 @@ const StyledDrawer = styled(Drawer)((
const drawerWidth = 240;
const GET_SETTINGS = gql`
query getGlobalSettings {
getGlobalSettings {
settings
}
}
`;
export function TopAppBar(props) {
const theme = useTheme();
const feedbackRef = React.useRef(null);
const settingsRef = React.useRef(null);
const documentationRef = React.useRef(null);
const [anchorEl, setAnchorEl] = React.useState(null);
const [documentationAnchorEl, setDocumentationAnchorEl] = React.useState(null);
const me = props.me;
const isOpen = useReactiveVar(menuOpen);
const [openGlobal, setOpenGlobal] = React.useState(false);
const [openCreate, setOpenCreate] = React.useState(false);
const [openOperations, setOpenOperations] = React.useState(false);
const [openData, setOpenData] = React.useState(false);
const [openFeedbackForm, setOpenFeedbackForm] = React.useState(false);
const [serverVersion, setServerVersion] = React.useState("...");
const [serverName, setServerName] = React.useState("...");
useQuery(GET_SETTINGS, {fetchPolicy: "no-cache",
onCompleted: (data) => {
setServerVersion(data.getGlobalSettings.settings["MYTHIC_SERVER_VERSION"]);
setServerName(data.getGlobalSettings.settings["MYTHIC_GLOBAL_SERVER_NAME"]);
}
});
const handleDrawerOpen = () => {
menuOpen(true);
};
@@ -160,20 +184,6 @@ export function TopAppBar(props) {
menuOpen(false);
};
const handleMenu = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = (evt) => {
setAnchorEl(null);
};
const handleDocumentationMenu = (event) => {
setDocumentationAnchorEl(event.currentTarget);
};
const handleDocumentationClose = (evt) => {
setDocumentationAnchorEl(null);
};
const handleToggleGlobal = () => {
setOpenGlobal(!openGlobal);
}
@@ -186,11 +196,6 @@ export function TopAppBar(props) {
const handleToggleData = () => {
setOpenData(!openData);
}
const handleLogout = () => {
menuOpen(false);
console.log("clicked logout, calling FailedRefresh");
FailedRefresh();
}
return (
<>
@@ -198,10 +203,10 @@ export function TopAppBar(props) {
<StyledAppBar className={clsx(classes.appBar, {[classes.appBarShift]: isOpen,})}>
{ me?.loggedIn ? (
<Toolbar variant="dense" >
<Toolbar variant="dense" style={{width: "100%"}} >
<IconButton
edge="start"
className={clsx(isOpen && classes.hide)}
className={clsx(isOpen && classes.hide, !isOpen && classes.menuButton)}
color="inherit"
aria-label="menu"
disableFocusRipple={true}
@@ -211,220 +216,16 @@ export function TopAppBar(props) {
<MenuIcon fontSize={"large"} />
</IconButton>
<div style={{width: "100%"}}>
<MythicStyledTooltip title="C2 Profiles and Payload Types">
<IconButton component={Link} to='/new/payloadtypes' color="inherit" size="large" disableFocusRipple={true}
disableRipple={true}>
<HeadsetTwoToneIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Payloads">
<IconButton component={Link} to='/new/payloads' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<FontAwesomeIcon size={"lg"} icon={faBiohazard} />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Search Operation">
<IconButton component={Link} to='/new/search?tab=callbacks&searchField=Host&search=' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<SearchIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Files">
<IconButton
component={Link}
to='/new/search?searchField=Filename&tab=files&location=Downloads'
color="inherit"
disableFocusRipple={true}
disableRipple={true}>
<AttachmentIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Artifacts">
<IconButton
disableFocusRipple={true}
disableRipple={true}
component={Link}
to='/new/search?searchField=Artifact&tab=artifacts'
color="inherit">
<FingerprintIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Proxies">
<IconButton component={Link} to='/new/search?tab=socks' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<FontAwesomeIcon size={"lg"} icon={faSocks} />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Screenshots" arrow >
<IconButton
component={Link}
to='/new/search?searchField=Filename&tab=files&location=Screenshots'
color="inherit"
disableFocusRipple={true}
disableRipple={true}
>
<CameraAltTwoToneIcon fontSize={"large"} />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Credentials" arrow >
<IconButton
component={Link}
to='/new/search?searchField=Account&tab=credentials'
color="inherit"
disableFocusRipple={true}
disableRipple={true}>
<VpnKeyIcon fontSize={"large"} className="mythicElement" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Active Callbacks">
<IconButton component={Link} to='/new/callbacks' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<PhoneCallbackIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Reporting" >
<IconButton component={Link} to='/new/reporting' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<FontAwesomeIcon size={"lg"} icon={faFlagCheckered} />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="MITRE ATT&CK" >
<IconButton component={Link} to='/new/mitre' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<TableChartTwoToneIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Operation Tags" >
<IconButton component={Link} to='/new/tagtypes' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<LocalOfferTwoToneIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<div className={'hideOnSmallWidth'}>
<TopBarShortcuts />
<Link style={{display: "inline-flex", alignItems: "center", paddingRight: "10px", color: "white", textDecoration: "none"}} to="/new/operations">
{me?.user?.current_operation || "No Operation Set"}
{me?.user?.current_operation || "No Operation Set"}
</Link>
<Menu
id="menu-appbar"
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
open={Boolean(anchorEl)}
onClose={handleClose}
MenuListProps={{style: {backgroundColor: theme.palette.primary.main, color: "white"}}}
>
<MenuItem divider={true} style={{display: "block"}} component={Link} to="/new/settings" onClick={handleClose} name="settings">
<Typography paragraph={true} variant="caption" style={{marginBottom: "0", color: "white"}}>Signed in as:</Typography>
<Typography paragraph={true} variant="body1" style={{marginBottom: "0", fontWeight: 600, color: "white"}}> {me?.user?.username || "" } </Typography>
</MenuItem>
<MenuItem component={Link} to="/new/login" onClick={handleLogout}>Logout</MenuItem>
</Menu>
<div style={{display: "inline-flex", justifyContent: "flex-end", float: "right"}}>
<IconButton
aria-label="feedback"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={() => setOpenFeedbackForm(true)}
ref={feedbackRef}
color="inherit"
style={{float: "right", }}
disableFocusRipple={true}
disableRipple={true}>
<MythicStyledTooltip title="Send Support Feedback">
<ThumbDownTwoTone fontSize={"large"} className="mythicElement" />
</MythicStyledTooltip>
</IconButton>
{openFeedbackForm &&
<MythicDialog fullWidth={true} maxWidth="md" open={openFeedbackForm}
onClose={()=>{setOpenFeedbackForm(false);}}
innerDialog={<MythicFeedbackDialog
title={"Submit Feedback via Webhook"}
onClose={()=>{setOpenFeedbackForm(false);}} />}
/>
}
<IconButton
aria-label="documentation links"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={handleDocumentationMenu}
ref={documentationRef}
color="inherit"
style={{float:"right"}}
disableFocusRipple={true}
disableRipple={true}>
<MythicStyledTooltip title="Help">
<HelpIcon fontSize={"large"} className="mythicElement"/>
</MythicStyledTooltip>
</IconButton>
<Menu
id="menu-appbar"
anchorEl={documentationAnchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
open={Boolean(documentationAnchorEl)}
onClose={handleDocumentationClose}
MenuListProps={{style: {backgroundColor: theme.palette.primary.main, color: "white"}}}
>
<MenuItem component={Link} target="_blank" to="/docs/agents" onClick={handleDocumentationClose}>Agent Documentation</MenuItem>
<MenuItem component={Link} target="_blank" to="/docs/wrappers" onClick={handleDocumentationClose}>Wrapper Documentation</MenuItem>
<MenuItem component={Link} target="_blank" to="/docs/c2-profiles" onClick={handleDocumentationClose}>C2 Profile Documentation</MenuItem>
<MenuItem component={Link} to={{pathname: "https://docs.mythic-c2.net"}} target="_blank" onClick={handleDocumentationClose}>Mythic Documentation</MenuItem>
</Menu>
<TopAppBarNotifications />
<IconButton
aria-label="account of current user"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={handleMenu}
ref={settingsRef}
color="inherit"
style={{float: "right", }}
disableFocusRipple={true}
disableRipple={true} >
<MythicStyledTooltip title="Settings or Logout">
<ManageAccountsIcon fontSize={"large"} className="mythicElement" />
</MythicStyledTooltip>
</IconButton>
{props.theme === 'dark' &&
<IconButton
disableFocusRipple={true}
disableRipple={true}
onClick={props.toggleTheme}
style={{float: "right", paddingRight: "0px", color: '#2f0e67'}}
</div>
>
<MythicStyledTooltip title="Change to Light Mode">
<DarkModeTwoToneIcon fontSize={"large"} className="mythicElement" />
</MythicStyledTooltip>
</IconButton>
}
{props.theme === 'light' &&
<IconButton
disableFocusRipple={true}
disableRipple={true}
onClick={props.toggleTheme}
style={{float: "right", paddingRight: "0px", color: '#eacc1b'}}
>
<MythicStyledTooltip title="Change to Dark Mode">
<LightModeTwoToneIcon fontSize={"large"} className="mythicElement" />
</MythicStyledTooltip>
</IconButton>
}
</div>
</div>
<TopBarRightShortcuts me={me} toggleTheme={props.toggleTheme} serverName={serverName} />
</Toolbar>
) : null
}
@@ -445,7 +246,6 @@ export function TopAppBar(props) {
{theme.direction === 'ltr' ? <ChevronLeftIcon className="mythicElement"/> : <ChevronRightIcon className="mythicElement"/>}
</IconButton>
</div>
<Divider />
<List
subheader={
<ListSubheader className={classes.listSubHeader} component="div" id="nested-list-subheader">
@@ -453,7 +253,7 @@ export function TopAppBar(props) {
</ListSubheader>
}>
<div style={{marginLeft: "15px"}}>
<b>Mythic Version:</b> v{mythicVersion}<br/>
<b>Mythic Version:</b> v{serverVersion}<br/>
<b>UI Version:</b> v{mythicUIVersion}<br/>
</div>
@@ -462,7 +262,6 @@ export function TopAppBar(props) {
<ListItemText primary={"Dashboard"} />
</ListItem>
</List>
<Divider />
<List
subheader={
<ListSubheader className={classes.listSubHeader} component="div" id="nested-list-subheader">
@@ -474,9 +273,8 @@ export function TopAppBar(props) {
<ListItemText>Services</ListItemText>
{openGlobal ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={openGlobal} unmountOnExit>
<List component="div" disablePadding>
<Collapse in={openGlobal} unmountOnExit >
<List component="div" disablePadding style={{border: 0}}>
<ListItem button className={classes.nested} target="_blank" component={Link} to='/jupyter' key={"jupyter"} onClick={handleDrawerClose}>
<ListItemIcon><CodeIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Jupyter Notebooks"} />
@@ -497,7 +295,7 @@ export function TopAppBar(props) {
{openCreate ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={openCreate} unmountOnExit>
<List component="div" disablePadding>
<List component="div" disablePadding style={{border: 0}}>
<ListItem button className={classes.nested} component={Link} to='/new/createpayload' key={"createpayload"} onClick={handleDrawerClose} state={{from: 'TopAppBar'}}>
<ListItemIcon><PostAddIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Create Payload"} />
@@ -514,7 +312,7 @@ export function TopAppBar(props) {
{openOperations ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={openOperations} unmountOnExit>
<List component="div" disablePadding>
<List component="div" disablePadding style={{border: 0}}>
<ListItem button className={classes.nested} component={Link} to='/new/payloadtypes' key={"payloadtypes"} onClick={handleDrawerClose}>
<ListItemIcon><HeadsetTwoToneIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Agents & C2"} />
@@ -530,7 +328,6 @@ export function TopAppBar(props) {
</List>
</Collapse>
</List>
<Divider />
<List
subheader={
<ListSubheader className={classes.listSubHeader} component="div" id="nested-list-subheader">
@@ -543,7 +340,7 @@ export function TopAppBar(props) {
{openData ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={openData} unmountOnExit>
<List component="div" disablePadding>
<List component="div" disablePadding style={{border: 0}}>
<ListItem button className={classes.nested} component={Link} to='/new/payloads' key={"payloads"} onClick={handleDrawerClose}>
<ListItemIcon><FontAwesomeIcon icon={faBiohazard} size="2x"/></ListItemIcon>
<ListItemText primary={"Payloads"} />
@@ -552,6 +349,22 @@ export function TopAppBar(props) {
<ListItemIcon><SearchIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Search"} />
</ListItem>
<ListItem button className={classes.nested} component={Link} to='/new/mitre' onClick={handleDrawerClose}>
<ListItemIcon><TableChartTwoToneIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"ATT&CK"} />
</ListItem>
<ListItem button className={classes.nested} component={Link} to='/new/reporting' onClick={handleDrawerClose}>
<ListItemIcon><FontAwesomeIcon size={"2x"} icon={faFlagCheckered} /></ListItemIcon>
<ListItemText primary={"Reporting"} />
</ListItem>
<ListItem button className={classes.nested} component={Link} to='/new/tagtypes' onClick={handleDrawerClose}>
<ListItemIcon><LocalOfferTwoToneIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Tags"} />
</ListItem>
<ListItem button className={classes.nested} component={Link} to='/new/eventing' onClick={handleDrawerClose}>
<ListItemIcon><PlayCircleFilledTwoToneIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Eventing"} />
</ListItem>
</List>
</Collapse>
<ListItem button component={Link} to='/new/callbacks' key={"callbacks"} onClick={handleDrawerClose}>
@@ -565,3 +378,256 @@ export function TopAppBar(props) {
);
}
function TopBarShortcuts({}){
return (
<>
<MythicStyledTooltip title="C2 Profiles and Payload Types" arrow >
<IconButton component={Link} to='/new/payloadtypes'
color="inherit"
size="large" disableFocusRipple={true}
disableRipple={true}>
<HeadsetTwoToneIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Payloads" arrow >
<IconButton component={Link} to='/new/payloads' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<FontAwesomeIcon size={"lg"} icon={faBiohazard} />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Search Operation" arrow >
<IconButton component={Link} to='/new/search?tab=callbacks&searchField=Host&search=' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<SearchIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Files" arrow >
<IconButton
component={Link}
to='/new/search?searchField=Filename&tab=files&location=Downloads'
color="inherit"
disableFocusRipple={true}
disableRipple={true}>
<AttachmentIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Artifacts" arrow >
<IconButton
disableFocusRipple={true}
disableRipple={true}
component={Link}
to='/new/search?searchField=Artifact&tab=artifacts'
color="inherit">
<FingerprintIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Proxies" arrow >
<IconButton component={Link} to='/new/search?tab=socks' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<FontAwesomeIcon size={"lg"} icon={faSocks} />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Screenshots" arrow >
<IconButton
component={Link}
to='/new/search?searchField=Filename&tab=files&location=Screenshots'
color="inherit"
disableFocusRipple={true}
disableRipple={true}
>
<CameraAltTwoToneIcon fontSize={"large"} />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Credentials" arrow >
<IconButton
component={Link}
to='/new/search?searchField=Account&tab=credentials'
color="inherit"
disableFocusRipple={true}
disableRipple={true}>
<VpnKeyIcon fontSize={"large"} className="mythicElement" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Active Callbacks" arrow >
<IconButton component={Link} to='/new/callbacks' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<PhoneCallbackIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Reporting" arrow >
<IconButton component={Link} to='/new/reporting' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<FontAwesomeIcon size={"lg"} icon={faFlagCheckered} />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="MITRE ATT&CK" arrow >
<IconButton component={Link} to='/new/mitre' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<TableChartTwoToneIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Operation Tags" arrow >
<IconButton component={Link} to='/new/tagtypes' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<LocalOfferTwoToneIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Eventing Scripts" arrow >
<TopAppBarEventingNotifications />
</MythicStyledTooltip>
</>
)
}
function TopBarRightShortcuts({me, toggleTheme, serverName}){
const theme = useTheme();
const feedbackRef = React.useRef(null);
const settingsRef = React.useRef(null);
const documentationRef = React.useRef(null);
const [anchorEl, setAnchorEl] = React.useState(null);
const [documentationAnchorEl, setDocumentationAnchorEl] = React.useState(null);
const [openFeedbackForm, setOpenFeedbackForm] = React.useState(false);
const handleLogout = () => {
menuOpen(false);
FailedRefresh();
}
const handleMenu = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = (evt) => {
setAnchorEl(null);
};
const handleDocumentationMenu = (event) => {
setDocumentationAnchorEl(event.currentTarget);
};
const handleDocumentationClose = (evt) => {
setDocumentationAnchorEl(null);
};
return (
<>
<Menu
id="menu-appbar"
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
open={Boolean(anchorEl)}
onClose={handleClose}
>
<MenuItem disabled divider={true} style={{display: "block"}}>
<Typography paragraph={true} variant={"caption"} style={{marginBottom: "0"}}>Server Name:</Typography>
<Typography paragraph={true} variant={"body1"} style={{marginBottom: "0", fontWeight: 600}}>{serverName}</Typography>
</MenuItem>
<MenuItem divider={true} style={{display: "block"}} component={Link} to="/new/settings" onClick={handleClose} name="settings">
<Typography paragraph={true} variant="caption" style={{marginBottom: "0"}}>Signed in as:</Typography>
<Typography paragraph={true} variant="body1" style={{marginBottom: "0", fontWeight: 600}}> {me?.user?.username || "" } </Typography>
</MenuItem>
<MenuItem onClick={handleLogout}>Logout</MenuItem>
</Menu>
<div style={{display: "inline-flex", justifyContent: "flex-end", float: "right"}}>
<IconButton
aria-label="feedback"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={() => setOpenFeedbackForm(true)}
ref={feedbackRef}
color="inherit"
style={{float: "right", }}
disableFocusRipple={true}
disableRipple={true}>
<MythicStyledTooltip title="Send Support Feedback">
<ThumbDownTwoTone fontSize={"large"} className="mythicElement" />
</MythicStyledTooltip>
</IconButton>
{openFeedbackForm &&
<MythicDialog fullWidth={true} maxWidth="md" open={openFeedbackForm}
onClose={()=>{setOpenFeedbackForm(false);}}
innerDialog={<MythicFeedbackDialog
title={"Submit Feedback via Webhook"}
onClose={()=>{setOpenFeedbackForm(false);}} />}
/>
}
<IconButton
aria-label="documentation links"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={handleDocumentationMenu}
ref={documentationRef}
color="inherit"
style={{float:"right"}}
disableFocusRipple={true}
disableRipple={true}>
<MythicStyledTooltip title="Help">
<HelpTwoToneIcon fontSize={"large"} className="mythicElement"/>
<KeyboardArrowDownIcon />
</MythicStyledTooltip>
</IconButton>
<Menu
id="menu-appbar"
anchorEl={documentationAnchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
open={Boolean(documentationAnchorEl)}
onClose={handleDocumentationClose}
>
<MenuItem component={Link} target="_blank" to="/docs/agents" onClick={handleDocumentationClose}>Agent Documentation</MenuItem>
<MenuItem component={Link} target="_blank" to="/docs/wrappers" onClick={handleDocumentationClose}>Wrapper Documentation</MenuItem>
<MenuItem component={Link} target="_blank" to="/docs/c2-profiles" onClick={handleDocumentationClose}>C2 Profile Documentation</MenuItem>
<MenuItem component={Link} to={{pathname: "https://docs.mythic-c2.net"}} target="_blank" onClick={handleDocumentationClose}>Mythic Documentation</MenuItem>
</Menu>
<TopAppBarEventLogNotifications />
<IconButton
aria-label="account of current user"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={handleMenu}
ref={settingsRef}
color="inherit"
style={{float: "right", }}
disableFocusRipple={true}
disableRipple={true} >
<MythicStyledTooltip title="Settings or Logout">
<ManageAccountsTwoToneIcon fontSize={"large"} className="mythicElement" />
<KeyboardArrowDownIcon />
</MythicStyledTooltip>
</IconButton>
{theme.palette.mode === 'dark' &&
<IconButton
disableFocusRipple={true}
disableRipple={true}
onClick={toggleTheme}
style={{float: "right", paddingRight: "0px", color: '#2f0e67'}}
>
<MythicStyledTooltip title="Change to Light Mode">
<DarkModeTwoToneIcon fontSize={"large"} className="mythicElement" />
</MythicStyledTooltip>
</IconButton>
}
{theme.palette.mode === 'light' &&
<IconButton
disableFocusRipple={true}
disableRipple={true}
onClick={toggleTheme}
style={{float: "right", paddingRight: "0px", color: '#eacc1b'}}
>
<MythicStyledTooltip title="Change to Dark Mode">
<LightModeTwoToneIcon fontSize={"large"} className="mythicElement" />
</MythicStyledTooltip>
</IconButton>
}
</div>
</>
)
}
@@ -5,24 +5,19 @@ import NotificationsActiveTwoToneIcon from '@mui/icons-material/NotificationsAct
import { Link } from 'react-router-dom';
import { IconButton } from '@mui/material';
import {snackActions} from './utilities/Snackbar';
import { meState } from '../cache';
import { useReactiveVar } from '@apollo/client';
import {MythicStyledTooltip} from "./MythicComponents/MythicStyledTooltip";
const SUB_Event_Logs = gql`
subscription MySubscription($operation_id: Int!) {
operation_by_pk(id: $operation_id) {
subscription OperationAlertCounts{
operation_stream(cursor: {initial_value: {updated_at: "1970-01-01"}, ordering: ASC}, batch_size: 1) {
id
alert_count
}
}
`;
export function TopAppBarNotifications(props) {
const me = useReactiveVar(meState);
export function TopAppBarEventLogNotifications(props) {
const { loading, error, data } = useSubscription(SUB_Event_Logs, {
variables: {operation_id: me?.user?.current_operation_id || 0},
onError: data => {
snackActions.error("Mythic encountered an error getting event log messages: " + data.toString());
console.error(data);
@@ -43,7 +38,10 @@ export function TopAppBarNotifications(props) {
<NotificationsActiveTwoToneIcon fontSize={"large"} />
</Badge>
) : (
<Badge badgeContent={data?.operation_by_pk?.alert_count || 0} color="error">
<Badge badgeContent={data?.operation_stream[0]?.alert_count || 0}
color="error" max={99}
sx={{marginTop: "3px"}}
>
<NotificationsActiveTwoToneIcon fontSize={"large"} />
</Badge>
)}
@@ -0,0 +1,65 @@
import React from 'react';
import {useSubscription, gql } from '@apollo/client';
import Badge from '@mui/material/Badge';
import PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwoTone';
import { Link } from 'react-router-dom';
import { IconButton } from '@mui/material';
import {snackActions} from './utilities/Snackbar';
import {MythicStyledTooltip} from "./MythicComponents/MythicStyledTooltip";
const OperationEventingCounts = gql`
subscription OperationEventingCounts {
eventgroupinstance_stream(cursor: {initial_value: {updated_at: "1970-01-01"}, ordering: ASC}, batch_size: 50) {
id
end_timestamp
}
}
`;
export function TopAppBarEventingNotifications(props) {
const [totalRunning, setTotalRunning] = React.useState([]);
const { loading, error, data } = useSubscription(OperationEventingCounts, {
onData: ({data}) => {
const newRunningCounts = data.data.eventgroupinstance_stream.reduce( (prev, cur) => {
let existingIndex = prev.findIndex( e => e.id === cur.id);
if(existingIndex >= 0){
if(cur.end_timestamp !== null){
prev.splice(existingIndex, 1);
}
return [...prev];
}
if(cur.end_timestamp === null){
return [...prev, cur];
}
return [...prev];
}, totalRunning);
setTotalRunning(newRunningCounts);
},
onError: data => {
snackActions.error("Mythic encountered an error getting eventing counts: " + data.toString());
console.error(data);
}
});
return (
<MythicStyledTooltip title="Eventing Counts">
<IconButton
color="inherit"
component={Link}
disableFocusRipple={true}
disableRipple={true}
to='/new/Eventing'>
{error ? (
<Badge color="secondary" badgeContent={0}>
<PlayCircleFilledTwoToneIcon fontSize={"large"} />
</Badge>
) : (
<Badge badgeContent={totalRunning.length} color="warning">
<PlayCircleFilledTwoToneIcon fontSize={"large"} />
</Badge>
)}
</IconButton>
</MythicStyledTooltip>
);
}
@@ -78,7 +78,7 @@ export function BrowserScripts({me}){
}
} )
setBrowserScripts(scripts);
if(backdropOpen){setBackdropOpen(false);}
setBackdropOpen(false);
}
});
const [toggleActive] = useMutation(updateBrowserScriptActive, {
@@ -117,6 +117,7 @@ export function BrowserScripts({me}){
}
});
const onToggleActive = ({browserscript_id, active}) => {
setBackdropOpen(true);
toggleActive({variables: {browserscript_id, active}});
}
const onSubmitEdit = ({browserscript_id, script, command_id, payload_type_id}) => {
@@ -20,7 +20,8 @@ export function BrowserScriptsTable(props){
const [openNewScriptDialog, setOpenNewScriptDialog] = React.useState(false);
return (
<React.Fragment>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main, marginBottom: "5px", marginTop: "10px", marginRight: "5px"}} variant={"elevation"}>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,
marginBottom: "5px", marginLeft: "5px", marginRight: "5px"}} variant={"elevation"}>
<Typography variant="h3" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
Browser Scripts
</Typography>
@@ -34,8 +35,8 @@ export function BrowserScriptsTable(props){
) : null
}
</Paper>
<TableContainer component={Paper} className="mythicElement" style={{maxHeight: "90%"}}>
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "100%", "overflow": "scroll"}}>
<TableContainer className="mythicElement" style={{maxHeight: "90%"}}>
<Table stickyHeader={true} size="small" style={{"tableLayout": "fixed", "maxWidth": "100%", "overflow": "scroll"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "3rem"}}>Edit</TableCell>
@@ -7,6 +7,7 @@ import {EditScriptDialog} from './EditScriptDialog';
import SettingsIcon from '@mui/icons-material/Settings';
import IconButton from '@mui/material/IconButton';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
export function BrowserScriptsTableRow(props){
const [openEdit, setOpenEdit] = React.useState(false);
@@ -29,17 +30,14 @@ export function BrowserScriptsTableRow(props){
<Switch
checked={props.active}
onChange={onToggleActive}
color="primary"
color="info"
inputProps={{ 'aria-label': 'checkbox', "track": "white" }}
name="Active"
/>
</TableCell>
<TableCell>
<MythicStyledTooltip title={props.payloadtype.name}>
<img
style={{width: "35px", height: "35px"}}
src={"/static/" + props.payloadtype.name + ".svg"}
/>
<MythicAgentSVGIcon payload_type={props.payloadtype.name} style={{width: "35px", height: "35px"}} />
</MythicStyledTooltip>
</TableCell>
<TableCell>{props.command.cmd}</TableCell>
@@ -111,7 +111,6 @@ export function EditScriptDialog(props) {
//setScript(atob(props.script));
setScript(decodeURIComponent(window.atob(props.script)));
}catch(error){
console.log(error)
setScript(props.script);
}
}
@@ -270,7 +269,7 @@ export function EditScriptDialog(props) {
input={<Input style={{width: "100%"}}/>}
>
{availableTasks.map( (opt) => (
<MenuItem value={opt} key={"task" + opt.id}>{opt.command_name + " " + opt.display_params}</MenuItem>
<MenuItem value={opt} key={"task" + opt.id}>{opt.command_name + " / " + opt.display_id + " / " + opt.display_params}</MenuItem>
) )}
</Select>
</FormControl>
@@ -19,12 +19,16 @@ query GetCallbackDetails($callback_id: Int!) {
loadedcommands {
command {
cmd
payloadtype {
name
}
id
}
id
}
payload {
payloadtype {
name
commands {
cmd
id
@@ -32,6 +36,13 @@ query GetCallbackDetails($callback_id: Int!) {
}
}
}
command(where: {payloadtype: {agent_type: {_eq: "command_augment"}}}){
cmd
id
payloadtype {
name
}
}
}
`;
@@ -46,18 +57,27 @@ export function AddRemoveCallbackCommandsDialog(props) {
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;
}
useQuery(getCommandsQuery, {variables: {callback_id: props.callback_id},
fetchPolicy: "no-cache",
onCompleted: (data) => {
setOriginalLeft(data.callback_by_pk.payload.payloadtype.commands);
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 = data.callback_by_pk.payload.payloadtype.commands.reduce( (prev, cur) => {
if( data.callback_by_pk.loadedcommands.filter(c => c.command.cmd === cur.cmd).length === 0){
const 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 {
return [...prev];
}
}, []);
leftData.sort( (a,b) => a.cmd < b.cmd ? -1 : 1);
setLeft(leftData);
const rightData = data.callback_by_pk.loadedcommands.map( c => c.command);
@@ -69,25 +89,15 @@ export function AddRemoveCallbackCommandsDialog(props) {
}
})
function not(a, b) {
if(props.itemKey){
return a.filter( (value) => b.find( (element) => element[props.itemKey] === value[props.itemKey] ) === undefined)
}
return a.filter((value) => b.indexOf(value) === -1);
}
function intersection(a, b) {
if(props.itemKey){
return a.filter( (value) => b.find( (element) => element[props.itemKey] === value[props.itemKey] ) !== undefined)
}
return a.filter((value) => b.indexOf(value) !== -1);
}
const handleToggle = (value) => () => {
let currentIndex = -1;
if(props.itemKey){
currentIndex = checked.findIndex( (element) => element[props.itemKey] === value[props.itemKey]);
}else{
currentIndex = checked.indexOf(value);
}
const newChecked = [...checked];
@@ -128,13 +138,13 @@ export function AddRemoveCallbackCommandsDialog(props) {
<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;
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 === value) !== -1}
checked={checked.findIndex( (element) => element.cmd + " (" + element?.payloadtype?.name + ")" === value) !== -1}
tabIndex={-1}
disableRipple
inputProps={{ 'aria-labelledby': labelId }}
@@ -152,10 +162,10 @@ export function AddRemoveCallbackCommandsDialog(props) {
const setFinalTags = () => {
// things to add are in the `right` now but weren't for `originalRight`
const commandsToAdd = right.filter( (command) => {
return originalRight.filter(orig => orig.command.cmd == command.cmd).length == 0;
return originalRight.filter(orig => orig.command.cmd === command.cmd && orig.command.payloadtype.name === command.payloadtype.name).length === 0;
});
const commandsToRemove = originalRight.filter( (command) => {
return right.filter(newCommand => newCommand.cmd == command.command.cmd).length == 0;
return right.filter(newCommand => newCommand.cmd === command.command.cmd && newCommand.payloadtype.name === command.command.payloadtype.name).length === 0;
})
props.onSubmit({commandsToAdd, commandsToRemove});
props.onClose();
@@ -344,7 +344,7 @@ export function C2PathDialog({callback, callbackgraphedges, onClose, onOpenTab})
</>
);
}
const getSourcePosition = (direction) => {
export const getSourcePosition = (direction) => {
if(direction === "RIGHT"){
return Position.Right
} else if(direction === "LEFT"){
@@ -357,7 +357,7 @@ const getSourcePosition = (direction) => {
return Position.Top
}
}
const getTargetPosition = (direction) => {
export const getTargetPosition = (direction) => {
if(direction === "RIGHT"){
return Position.Left
} else if(direction === "LEFT"){
@@ -384,7 +384,7 @@ function AgentNode({data}) {
}
const additionalStyles = data?.selected ? {
boxShadow: `5px 5px 25px 5px ${theme.palette.info.main}, inset 0px 0px 60px 0px ${theme.palette.info.main}`,
boxShadow: `3px 3px 5px 0px ${theme.palette.secondary.main}, inset 0px 0px 60px 0px ${theme.palette.info.main}`,
borderRadius: "20px"
} : {};
return (
@@ -436,7 +436,7 @@ function BrowserscriptNode({data}) {
</div>
)
}
function GroupNode({data}) {
export function GroupNode({data}) {
const sourcePosition = getSourcePosition(data["elk.direction"]);
const targetPosition = getTargetPosition(data["elk.direction"]);
return (
@@ -461,6 +461,9 @@ const getWidth = (node) => {
if(node.type === "browserscriptNode"){
return getBrowserscriptWidth(node);
}
if(node.type === "eventNode"){
return getEventNodeWidth(node);
}
return Math.max(100, node.data.label.length * 7);
}
const getTaskWidth = (node) => {
@@ -470,6 +473,9 @@ const getTaskWidth = (node) => {
}
return Math.max(325, (nodeText.length * 8) + 10)
}
const getEventNodeWidth = (node) => {
return (node.maxNameLength * 8) + 10 + 100;
}
const getBrowserscriptWidth = (node) => {
let nodeText = " ";
if(node?.data?.width){
@@ -484,9 +490,12 @@ const getHeight = (node) => {
if(node.hidden){
return 0;
}
if(node.type === "eventNode"){
return 20;
}
return 80;
}
export default async function createLayout({initialGroups, initialNodes, initialEdges, alignment}) {
export default async function createLayout({initialGroups, initialNodes, initialEdges, alignment, elkOverwrites}) {
let elkAlignment = {
"elk.alignment": "RIGHT" , //LEFT, RIGHT, TOP, BOTTOM, CENTER
"elk.direction": "RIGHT" , //DOWN, LEFT, RIGHT, UP
@@ -501,6 +510,14 @@ export default async function createLayout({initialGroups, initialNodes, initial
"elk.alignment": "BOTTOM", //LEFT, RIGHT, TOP, BOTTOM, CENTER
"elk.direction": "DOWN", //DOWN, LEFT, RIGHT, UP
}
}else if(alignment === "RL"){
elkAlignment = {
"elk.alignment": "RIGHT" , //LEFT, RIGHT, TOP, BOTTOM, CENTER
"elk.direction": "LEFT" , //DOWN, LEFT, RIGHT, UP
}
}
if(elkOverwrites === undefined){
elkOverwrites = {};
}
const options = {
"elk.algorithm": "layered",
@@ -519,6 +536,7 @@ export default async function createLayout({initialGroups, initialNodes, initial
"elk.layered.spacing.edgeEdgeBetweenLayers": 20,
"elk.layered.spacing.edgeNodeBetweenLayers": 40,
"elk.layered.spacing.baseValue": 40,
...elkOverwrites
}
const graph = {
id: "root",
@@ -878,7 +896,7 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, theme, contex
extent: shouldUseGroups(view_config) ? "parent" : null,
data: {
label: getLabel(node, view_config["label_components"]),
img: "/static/" + node.payload.payloadtype.name + ".svg",
img: "/static/" + node.payload.payloadtype.name + "_" + theme.palette.mode + ".svg",
isMythic: false,
callback_id: node.id,
display_id: node.display_id,
@@ -1345,7 +1363,7 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, theme, contex
{openContextMenu &&
<div style={{...contextMenuCoord, position: "fixed"}} className="context-menu">
{contextMenu.map( (m) => (
<Button key={m.title} variant={"contained"} className="context-menu-button" onClick={() => {
<Button key={m.title} color={"info"} className="context-menu-button" onClick={() => {
m.onClick(contextMenuNode.current);
setOpenContextMenu(false);
}}>{m.title}</Button>
@@ -1649,7 +1667,7 @@ export const DrawTaskElementsFlow = ({edges, panel, view_config, theme, contextM
{openContextMenu &&
<div style={{...contextMenuCoord, position: "fixed"}} className="context-menu">
{contextMenu.map( (m) => (
<Button key={m.title} variant={"contained"} className="context-menu-button" onClick={() => {
<Button key={m.title} color={"info"} className="context-menu-button" onClick={() => {
m.onClick(contextMenuNode.current);
setOpenContextMenu(false);
}}>{m.title}</Button>
@@ -2341,7 +2359,7 @@ const DrawBrowserScriptElementsFlow = ({edges, panel, view_config, theme, contex
{openContextMenu &&
<div style={{...contextMenuCoord, position: "fixed"}} className="context-menu">
{localContextMenu.map( (m) => (
<Button key={m?.key ? m.key : m.title} variant={"contained"} className="context-menu-button" onClick={() => {
<Button key={m?.key ? m.key : m.title} color={"info"} className="context-menu-button" onClick={() => {
m.onClick(contextMenuNode.current);
setOpenContextMenu(false);
}}>{m.title}</Button>
@@ -101,6 +101,17 @@ export const taskingDataFragment = gql`
commentOperator{
username
}
eventstepinstance {
eventgroupinstance {
eventgroup{
id
}
id
}
eventstep {
name
}
}
completed
id
display_id
@@ -122,6 +133,7 @@ export const taskingDataFragment = gql`
opsec_post_blocked
opsec_post_bypassed
interactive_task_type
has_intercepted_response
tasks(where: {is_interactive_task: {_eq: false}}, order_by: {id: asc}) {
id
comment
@@ -166,6 +178,7 @@ export const taskingDataFragment = gql`
tasks(order_by: {id: asc}) {
id
}
has_intercepted_response
}
response_count
tags {
@@ -182,8 +195,8 @@ export const taskingDataFragment = gql`
}
`;
export const createTaskingMutation = gql`
mutation createTasking($callback_id: Int, $callback_ids: [Int], $command: String!, $params: String!, $files: [String], $token_id: Int, $tasking_location: String, $original_params: String, $parameter_group_name: String, $parent_task_id: Int, $is_interactive_task: Boolean, $interactive_task_type: Int) {
createTask(callback_id: $callback_id, callback_ids: $callback_ids, command: $command, params: $params, files: $files, token_id: $token_id, tasking_location: $tasking_location, original_params: $original_params, parameter_group_name: $parameter_group_name, parent_task_id: $parent_task_id, is_interactive_task: $is_interactive_task, interactive_task_type: $interactive_task_type) {
mutation createTasking($callback_id: Int, $callback_ids: [Int], $command: String!, $params: String!, $files: [String], $token_id: Int, $tasking_location: String, $original_params: String, $parameter_group_name: String, $parent_task_id: Int, $is_interactive_task: Boolean, $interactive_task_type: Int, $payload_type: String) {
createTask(callback_id: $callback_id, callback_ids: $callback_ids, command: $command, params: $params, files: $files, token_id: $token_id, tasking_location: $tasking_location, original_params: $original_params, parameter_group_name: $parameter_group_name, parent_task_id: $parent_task_id, is_interactive_task: $is_interactive_task, interactive_task_type: $interactive_task_type, payload_type: $payload_type) {
status
id
error
@@ -182,6 +182,7 @@ export function Callbacks({me}) {
onEditTabDescription={onEditTabDescription}
key={'callbackstabs'}
clickedTabId={clickedTabId}
setClickedTabId={setClickedTabId}
openTabs={openTabs}
onDragTab={onDragTab}
me={me}
@@ -10,7 +10,6 @@ import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import {useMutation } from '@apollo/client';
import {saveSvgAsPng} from 'save-svg-as-png';
import {hideCallbackMutation, removeEdgeMutation, addEdgeMutation} from './CallbackMutations';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
import {MythicSelectFromListDialog} from '../../MythicComponents/MythicSelectFromListDialog';
@@ -211,11 +210,10 @@ const GraphViewOptions = ({viewConfig, setViewConfig}) => {
</div>
)
}
export function CallbacksGraph({}){
export function CallbacksGraph({onOpenTab}){
const theme = useTheme();
const callbacks = useContext(CallbacksContext);
const callbackgraphedges = useContext(CallbackGraphEdgesContext);
const onOpenTab = useContext(OnOpenTabContext);
//used for creating a task to do a link command
const [linkCommands, setLinkCommands] = React.useState([]);
const [openParametersDialog, setOpenParametersDialog] = React.useState(false);
@@ -331,7 +329,6 @@ export function CallbacksGraph({}){
{
title: 'Interact',
onClick: function(node){
console.log(node);
onOpenTab({tabType: "interact", tabID: node.callback_id + "interact", callbackID: node.callback_id});
}
},
@@ -28,6 +28,7 @@ import {
} from 'material-react-table';
import {useTheme} from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import {SetMythicSetting, useMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
export const ipCompare = (a, b) => {
let aJSON = JSON.parse(a);
@@ -146,7 +147,7 @@ function CallbacksTablePreMemo(props){
{key: "sleep", type: 'string', name: "Sleep", width: 75, disableSort: true},
{key: "agent", type: 'string', name: "Agent", width: 100, disableSort: true},
{key: "c2", type: 'string', name: "C2", width: 75, disableSort: true, disableFilterMenu: true},
{key: "process_name", type: 'string', name: "Process Name", fillWidth: true},
{key: "process_short_name", type: 'string', name: "Process Name", fillWidth: true},
].reduce( (prev, cur) => {
if(columnVisibility.visible.includes(cur.name) || cur.name === "Interact"){
if(filterOptions[cur.key] && String(filterOptions[cur.key]).length > 0){
@@ -277,7 +278,6 @@ function CallbacksTablePreMemo(props){
return <CallbacksTableIDCell
rowData={row}
key={`callback${row.id}_${c.name}`}
//onOpenTab={props.onOpenTab}
updateDescription={updateDescriptionSubmit}
setOpenHideMultipleDialog={setOpenHideMultipleDialog}
setOpenTaskMultipleDialog={setOpenTaskMultipleDialog}
@@ -311,7 +311,7 @@ function CallbacksTablePreMemo(props){
case "C2":
return <CallbacksTableC2Cell key={`callback${row.id}_c2`} rowData={row} />
case "Process Name":
return <CallbacksTableStringCell key={`callback${row.id}_${c.name}`} cellData={row.process_name} rowData={row} />;
return <CallbacksTableStringCell key={`callback${row.id}_${c.name}`} cellData={row.process_short_name} rowData={row} />;
}
})];
}
@@ -458,73 +458,50 @@ function CallbacksTableMaterialReactTablePreMemo(props){
{key: "os", type: 'string', name: "OS", width: 75, disableCopy: true, enableHiding: true},
{key: "architecture", type: 'string', name: "Arch", width: 75, enableHiding: true},
{key: "pid", type: 'number', name: "PID", width: 75, enableHiding: true},
{key: "last_checkin", type: 'timestamp', name: "Last Checkin", width: 150, disableFilterMenu: true, enableHiding: true},
{key: "last_checkin", type: 'timestamp', name: "Last Checkin", width: 150, disableFilterMenu: true, disableCopy: true, enableHiding: true},
{key: "description", type: 'string', name: "Description", width: 400, enableHiding: true},
{key: "sleep", type: 'string', name: "Sleep", width: 75, disableSort: true, disableCopy: true, enableHiding: true},
{key: "agent", type: 'string', name: "Agent", width: 100, disableSort: true, disableCopy: true, enableHiding: true},
{key: "c2", type: 'string', name: "C2", width: 75, disableSort: true, disableFilterMenu: true, disableCopy: true, enableHiding: true},
{key: "process_name", type: 'string', name: "Process Name", fillWidth: true, enableHiding: true},
{key: "process_short_name", type: 'string', name: "Process Name", fillWidth: true, enableHiding: true},
];
const [columnVisibility, setColumnVisibility] = React.useState({
id: true,
host: true,
domain:true,
user:true,
description:true,
last_checkin: true,
agent: true,
ip: true,
pid: true,
architecture: false,
sleep: false,
process_name: false,
external_ip: false,
c2: true,
os: false,
mythictree_groups: false
});
React.useEffect( () => {
// on startup, want to see if `callbacks_table_columns` exists in storage and load it if possible
try {
const storageItem = localStorage.getItem("callbacks_table_columns");
if(storageItem !== null){
let loadedColumnNames = JSON.parse(storageItem);
if(loadedColumnNames === null){return}
let currentVisibility = {...columnVisibility};
for( const[key, val] of Object.entries(currentVisibility)){
let col = columnFields.filter( c => c.key === key)[0];
currentVisibility[key] = !!loadedColumnNames.includes(col.name);
}
setColumnVisibility(currentVisibility);
}
}catch(error){
console.log("Failed to load callbacks_table_columns", error);
}
}, [])
const onColumnVisibilityChange = (visibility) => {
setColumnVisibility(visibility);
}
const initialColumnVisibility = useMythicSetting({setting_name: "callbacks_table_columns",
output: "json",
default_value: {
id: true,
host: true,
domain:true,
user:true,
description:true,
last_checkin: true,
agent: true,
ip: true,
pid: true,
architecture: false,
sleep: false,
process_short_name: false,
external_ip: false,
c2: true,
os: false,
mythictree_groups: false
}});
const initialColumnFilters = useMythicSetting({setting_name: "callbacks_table_filters",
output: "json-array",
default_value: []});
const [columnVisibility, setColumnVisibility] = React.useState(initialColumnVisibility);
const [columnFilters, setColumnFilters] = React.useState(initialColumnFilters);
React.useEffect( () => {
let shown = [];
for(const [key, val] of Object.entries(columnVisibility)){
if(val){
let newKey = columnFields.filter(c => c.key === key);
if(newKey.length > 0){
shown.push(newKey[0].name);
}
}
}
localStorage.setItem("callbacks_table_columns", JSON.stringify(shown));
}, [columnVisibility]);
SetMythicSetting({setting_name: "callbacks_table_columns",
value: columnVisibility, output:"json"});
SetMythicSetting({setting_name: "callbacks_table_filters",
value: columnFilters, output: "json-array"});
}, [columnVisibility, columnFilters]);
const localCellRender = React.useCallback( ({cell, h}) => {
let row = cell.row?.original;
switch(h.name){
case "Interact":
return <CallbacksTableIDCell
rowData={row}
//onOpenTab={props.onOpenTab}
updateDescription={updateDescriptionSubmit}
setOpenHideMultipleDialog={setOpenHideMultipleDialog}
setOpenTaskMultipleDialog={setOpenTaskMultipleDialog}
@@ -558,7 +535,7 @@ function CallbacksTableMaterialReactTablePreMemo(props){
case "C2":
return <CallbacksTableC2Cell rowData={row} />
case "Process Name":
return <CallbacksTableStringCell cellData={row.process_name} rowData={row} />;
return <CallbacksTableStringCell cellData={row.process_short_name} rowData={row} />;
}
}, []);
const columns = React.useMemo( () => columnFields.map( h => {
@@ -598,9 +575,12 @@ function CallbacksTableMaterialReactTablePreMemo(props){
//columnResizeMode: 'onEnd',
initialState: {
density: 'compact',
columnVisibility,
columnFilters
},
state: {columnVisibility},
onColumnVisibilityChange: onColumnVisibilityChange,
state: {columnVisibility, columnFilters},
onColumnVisibilityChange: setColumnVisibility,
onColumnFiltersChange: setColumnFilters,
defaultDisplayColumn: { enableResizing: true },
muiTableContainerProps: { sx: { alignItems: "flex-start" } },
mrtTheme: (theme) => ({
@@ -49,6 +49,7 @@ import WidgetsIcon from '@mui/icons-material/Widgets';
import {ModifyCallbackMythicTreeGroupsDialog} from "./ModifyCallbackMythicTreeGroupsDialog";
import TerminalIcon from '@mui/icons-material/Terminal';
import ImportExportIcon from '@mui/icons-material/ImportExport';
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
export const CallbacksTableIDCell = React.memo(({rowData, toggleLock, updateDescription, setOpenHideMultipleDialog, setOpenTaskMultipleDialog}) =>{
const dropdownAnchorRef = React.useRef(null);
@@ -194,12 +195,12 @@ export const CallbacksTableIDCell = React.memo(({rowData, toggleLock, updateDesc
}
})
const options = [
{name: 'Hide Callback', icon: <VisibilityOffIcon style={{color: theme.palette.warning.main, paddingRight: "5px"}}/>, click: (evt) => {
{name: 'Hide Callback', icon: <VisibilityOffIcon color={"warning"} style={{paddingRight: "5px"}}/>, click: (evt) => {
evt.stopPropagation();
hideCallback({variables: {callback_display_id: rowDataStatic.display_id}});
}},
{
name: "Hide Multiple", icon: <VisibilityOffIcon style={{paddingRight: "5px"}}/>, click: (evt) => {
name: "Hide Multiple", icon: <VisibilityOffIcon color={"warning"} style={{paddingRight: "5px"}}/>, click: (evt) => {
setOpenHideMultipleDialog(true);
}
},
@@ -231,7 +232,7 @@ export const CallbacksTableIDCell = React.memo(({rowData, toggleLock, updateDesc
localOnOpenTab("interactConsole");
}
},
{name: rowDataStatic.locked ? 'Unlock (Locked by ' + rowDataStatic.locked_operator.username + ')' : 'Lock Callback', icon: rowDataStatic.locked ? (<LockOpenIcon style={{color: theme.successOnMain, paddingRight: "5px"}}/>) : (<LockIcon style={{color: theme.errorOnMain, paddingRight: "5px"}} />), click: (evt) => {
{name: rowDataStatic.locked ? 'Unlock (Locked by ' + rowDataStatic.locked_operator.username + ')' : 'Lock Callback', icon: rowDataStatic.locked ? (<LockIcon color={"error"} style={{paddingRight: "5px"}}/>) : (<LockOpenIcon color={"success"} style={{paddingRight: "5px"}} />), click: (evt) => {
evt.stopPropagation();
localToggleLock();
}},
@@ -247,11 +248,11 @@ export const CallbacksTableIDCell = React.memo(({rowData, toggleLock, updateDesc
evt.stopPropagation();
exportConfig({variables: {agent_callback_id: rowDataStatic.agent_callback_id}});
}},
{name: "View Metadata", icon: <InfoIcon style={{color: theme.infoOnMain, paddingRight: "5px"}} />, click: (evt) => {
{name: "View Metadata", icon: <InfoIcon color={"info"} style={{paddingRight: "5px"}} />, click: (evt) => {
evt.stopPropagation();
setOpenMetaDialog(true);
}},
{name: "Modify Groupings", icon: <WidgetsIcon style={{paddingRight: "5px"}} />, click: (evt) => {
{name: "Modify Groupings", icon: <WidgetsIcon color={"info"} style={{paddingRight: "5px"}} />, click: (evt) => {
evt.stopPropagation();
setOpenEditMythicTreeGroupsDialog(true);
}}
@@ -265,7 +266,7 @@ export const CallbacksTableIDCell = React.memo(({rowData, toggleLock, updateDesc
>
<Button style={{padding: "0 10px 0 10px"}} color={rowDataStatic.integrity_level > 2 ? "error" : "primary"} variant="contained"
onClick={(evt) => {evt.stopPropagation();localOnOpenTab("interact")}}>
{ rowDataStatic.locked ? (<LockIcon fontSize="large" style={{color: theme.errorOnMain, marginRight: "10px"}} />):(<KeyboardIcon fontSize="large" style={{marginRight: "10px"}}/>) }
{ rowDataStatic.locked ? (<LockIcon fontSize="large" style={{marginRight: "10px"}} />):(<KeyboardIcon fontSize="large" style={{marginRight: "10px"}}/>) }
{rowDataStatic.display_id}
</Button>
<Button
@@ -359,20 +360,39 @@ export const CallbacksTableStringCell = React.memo(({rowData, cellData}) => {
}, areEqual)
export const CallbacksTableLastCheckinCell = React.memo( ({rowData, cellData}) => {
const adjustOutput = (newTime) => {
if(newTime.includes("54 years")){
return "Streaming Now"
}else if(newTime === "a few seconds"){
if(newTime === "a few seconds"){
moment.relativeTimeThreshold('s', 60);
moment.relativeTimeThreshold('ss', 0);
return moment(rowData.last_checkin + "Z", "YYYY-MM-DDTHH:mm:ss.SSSSSSZ").fromNow(true)
}
return newTime;
}
const theme = useTheme();
if(rowData?.payload?.payloadtype?.agent_type !== "agent"){
return ""
}
if(rowData.last_checkin === "1970-01-01T00:00:00"){
return (
<>
{rowData.dead &&
<MythicStyledTooltip title={"Based on callback's last checkin and sleep info, it's likely dead"}>
<FontAwesomeIcon disabled icon={faSkullCrossbones} style={{
color: theme.palette.error.main, cursor: "pointer", marginRight: "10px",}} />
</MythicStyledTooltip>
}
{"Streaming Now"}
</>
)
}
return (
<div>
<div style={{display: "flex", alignItems: "center"}}>
{rowData.dead &&
<MythicStyledTooltip title={"Based on callback's last checkin and sleep info, it's likely dead"}>
<FontAwesomeIcon icon={faSkullCrossbones} style={{
color: theme.palette.error.main, cursor: "pointer", marginRight: "10px",}} />
</MythicStyledTooltip>
}
<Moment filter={adjustOutput} interval={1000} parse={"YYYY-MM-DDTHH:mm:ss.SSSSSSZ"}
withTitle
titleFormat={"YYYY-MM-DD HH:mm:ss"}
@@ -387,10 +407,7 @@ export const CallbacksTableLastCheckinCell = React.memo( ({rowData, cellData}) =
export const CallbacksTablePayloadTypeCell = React.memo( ({rowData}) => {
return (
<MythicStyledTooltip title={rowData?.payload?.payloadtype?.name}>
<img
style={{width: "35px", height: "35px"}}
src={"/static/" + rowData?.payload?.payloadtype?.name + ".svg"}
/>
<MythicAgentSVGIcon payload_type={rowData?.payload?.payloadtype?.name} style={{width: "35px", height: "35px"}} />
</MythicStyledTooltip>
)
}, areEqual)
@@ -645,15 +662,18 @@ export const CallbacksTableSleepCell = React.memo( ({rowData, cellData, updateSl
return null
}
return (
<div>
<div style={{height: "100%"}}>
<SnoozeIcon onClick={onOpenSleepDialog}
style={{color: cellData === "" ? theme.palette.warning.main : theme.palette.info.main, cursor: "pointer"}}
/>
{ openSleepDialog &&
<MythicDialog fullWidth={true} open={openSleepDialog} onClose={() => {setOpenSleepDialog(false);}}
<MythicDialog fullWidth={true} open={openSleepDialog} maxWidth={"md"}
style={{height: "100%"}}
onClose={() => {setOpenSleepDialog(false);}}
innerDialog={
<MythicModifyStringDialog title={"View Sleep Information"} multiline maxRows={10}
onClose={() => {setOpenSleepDialog(false);}}
<MythicModifyStringDialog title={"View / Edit Sleep Information"}
maxRows={20}
onClose={() => {setOpenSleepDialog(false);}}
value={cellData}
onSubmit={editSleepSubmit} />
}
@@ -7,7 +7,7 @@ import { CallbacksTabsProcessBrowserLabel, CallbacksTabsProcessBrowserPanel } fr
import { CallbacksTabsTaskingSplitLabel, CallbacksTabsTaskingSplitPanel} from "./CallbacksTabsTaskingSplit";
import {CallbacksTabsTaskingConsoleLabel, CallbacksTabsTaskingConsolePanel} from "./CallbacksTabsTaskingConsole";
export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, onEditTabDescription, contextMenuOptions, me}) {
export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, setClickedTabId, onEditTabDescription, contextMenuOptions, me}) {
const mountedRef = React.useRef(true);
const [value, setValue] = React.useState(0);
@@ -15,6 +15,7 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, o
setValue(newValue);
//console.log(newValue);
localStorage.setItem('clickedTab', openTabs[newValue].tabID);
setClickedTabId(openTabs[newValue].tabID);
};
React.useEffect( () => {
return() => {
@@ -140,7 +140,7 @@ export function CallbacksTabsFileBrowserLabel(props) {
);
}
export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) => {
const [fromNow, setFromNow] = React.useState((new Date()));
const fromNow = React.useRef((new Date()));
const [backdropOpen, setBackdropOpen] = React.useState(false);
const treeRootDataRef = React.useRef({}); // hold all of the actual data
const [treeAdjMtx, setTreeAdjMtx] = React.useState({}); // hold the simple adjacency matrix for parent/child relationships
@@ -196,7 +196,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
fetchPolicy: 'no-cache',
});
useSubscription(fileDataSubscription, {
variables: {now: fromNow},
variables: {now: fromNow.current},
fetchPolicy: "no-cache",
onData: ({data}) => {
for(let i = 0; i < data.data.mythictree_stream.length; i++){
@@ -468,13 +468,14 @@ const FileBrowserTableTop = ({
};
return (
<Grid container spacing={0} style={{ paddingTop: '10px' }}>
<Grid item xs={12}>
<Grid item xs={12} >
<MythicTextField
placeholder={selectedFolderData.host}
value={fullPath}
onEnter={onLocalListFilesButton}
onChange={onChangePath}
name={placeHolder}
InputProps={{
endAdornment: (
<React.Fragment>
@@ -508,7 +509,7 @@ const FileBrowserTableTop = ({
<CallbacksTabsTaskingInputTokenSelect options={tokenOptions} changeSelectedToken={changeSelectedToken}/>
) : null}
</React.Fragment>),
style: { padding: 0 },
style: { },
}}
/>
</Grid>
@@ -423,30 +423,6 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
const [downloadHistory, setDownloadHistory] = React.useState([]);
const [getPermissions] = useLazyQuery(getPermissionsDataQuery, {
onCompleted: (data) => {
let newPermissions = {};
/*
Object.keys(data.mythictree_by_pk.metadata).forEach( (key) => {
if( key.includes("time") ){
try{
newPermissions[key] = TableRowDateCell({cellData: data.mythictree_by_pk.metadata[key], view_utc_time: me?.user?.view_utc_time})
}catch(error){
console.log("failed to parse metadata as date", key, data.mythictree_by_pk.metadata[key]);
newPermissions[key] = data.mythictree_by_pk.metadata[key];
}
} else if( key.includes("size") ){
try{
console.log(data.mythictree_by_pk.metadata, data.mythictree_by_pk.metadata[key])
newPermissions[key] = TableRowSizeCell({cellData: data.mythictree_by_pk.metadata[key]})
}catch(error){
console.log("failed to parse metadata as size", key, data.mythictree_by_pk.metadata[key]);
newPermissions[key] = data.mythictree_by_pk.metadata[key];
}
} else {
newPermissions[key] = data.mythictree_by_pk.metadata[key];
}
});
*/
setPermissionData(data.mythictree_by_pk.metadata);
if (data.mythictree_by_pk.metadata !== '') {
setViewPermissionsDialogOpen(true);
@@ -598,7 +574,7 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
aria-expanded={dropdownOpen ? 'true' : undefined}
aria-haspopup='menu'
onClick={handleDropdownToggle}
color='primary'
color='info'
variant='contained'
ref={dropdownAnchorRef}>
<SettingsIcon />
@@ -8,6 +8,7 @@ import {hideCallbacksMutation} from './CallbackMutations';
import {snackActions} from "../../utilities/Snackbar";
import {CallbacksTableLastCheckinCell, CallbacksTablePayloadTypeCell, CallbacksTableIPCell} from "./CallbacksTableRow";
import { DataGrid } from '@mui/x-data-grid';
import {useTheme} from '@mui/material/styles';
const callbacksAndFeaturesQuery = gql`
@@ -22,10 +23,12 @@ query callbacksAndFeatures{
pid
description
last_checkin
dead
payload {
payloadtype {
name
id
agent_type
}
}
mythictree_groups_string
@@ -92,6 +95,7 @@ const columns = [
];
const CustomSelectTable = ({initialData, selectedData}) => {
const [data, setData] = React.useState([]);
const theme = useTheme();
const [rowSelectionModel, setRowSelectionModel] = React.useState([]);
React.useEffect( () => {
selectedData.current = data.reduce( (prev, cur) => {
@@ -107,6 +111,7 @@ const CustomSelectTable = ({initialData, selectedData}) => {
return (
<div style={{height: "calc(80vh)"}}>
<DataGrid
style={{backgroundColor: theme.palette.background.paper}}
rows={data}
columns={columns}
initialState={{
@@ -56,11 +56,11 @@ fragment treeObjData on mythictree {
`;
const treeSubscription = gql`
${treeFragment}
subscription liveData($now: timestamp!, $operation_id: Int!) {
subscription liveData($now: timestamp!) {
mythictree_stream(
batch_size: 1000,
cursor: {initial_value: {timestamp: $now}},
where: { operation_id: { _eq: $operation_id }, tree_type: {_eq: "process"} }
cursor: {initial_value: {timestamp: $now}},
where: {tree_type: {_eq: "process"} }
) {
...treeObjData
}
@@ -68,8 +68,8 @@ const treeSubscription = gql`
`;
const rootQuery = gql`
${treeFragment}
query myRootFolderQuery($operation_id: Int!) {
mythictree(where: { operation_id: { _eq: $operation_id }, tree_type: {_eq: "process"} }, order_by: {id: asc}) {
query myRootFolderQuery {
mythictree(where: { tree_type: {_eq: "process"} }, order_by: {id: asc}) {
...treeObjData
}
}
@@ -117,7 +117,7 @@ export function CallbacksTabsProcessBrowserLabel(props){
)
}
export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>{
const [fromNow, setFromNow] = React.useState((new Date()));
const fromNow = React.useRef((new Date()));
const treeRootDataRef = React.useRef({}); // hold all the actual data
const [treeAdjMtx, setTreeAdjMtx] = React.useState({}); // hold the simple adjacency matrix for parent/child relationships
const [openTaskingButton, setOpenTaskingButton] = React.useState(false);
@@ -193,7 +193,7 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>
fetchPolicy: 'no-cache',
});
useSubscription(treeSubscription, {
variables: {now: fromNow, operation_id: me?.user?.current_operation_id ||0},
variables: {now: fromNow.current},
fetchPolicy: "no-cache",
onData: ({data}) => {
for(let i = 0; i < data.data.mythictree_stream.length; i++){
@@ -698,7 +698,6 @@ const FileBrowserTableRowActionCell = ({rowData, onTaskRowAction, treeRootData,
const [fileCommentDialogOpen, setFileCommentDialogOpen] = React.useState(false);
const [getPermissions] = useLazyQuery(getPermissionsDataQuery, {
onCompleted: (data) => {
console.log(rowData);
setPermissionData({...data.mythictree_by_pk.metadata,
callback_id: rowData["callback_id"],
callback_display_id: rowData["callback_display_id"]});
@@ -9,6 +9,8 @@ import {CallbacksTabsTaskingInput} from "./CallbacksTabsTaskingInput";
import {CallbacksTableIPCell, CallbacksTableLastCheckinCell} from "./CallbacksTableRow";
import { DataGrid } from '@mui/x-data-grid';
import { validate as uuidValidate } from 'uuid';
import {snackActions} from "../../utilities/Snackbar";
import {useTheme} from '@mui/material/styles';
const callbacksAndFeaturesQuery = gql`
@@ -24,7 +26,13 @@ query callbacksAndFeatures($payloadtype_id: Int!) {
display_id
last_checkin
ip
dead
mythictree_groups_string
payload {
payloadtype {
agent_type
}
}
}
}`;
@@ -81,6 +89,7 @@ const columns = [
];
const CustomSelectTable = ({initialData, selectedData}) => {
const [data, setData] = React.useState([]);
const theme = useTheme();
const [rowSelectionModel, setRowSelectionModel] = React.useState([]);
React.useEffect( () => {
selectedData.current = data.reduce( (prev, cur) => {
@@ -96,6 +105,7 @@ const CustomSelectTable = ({initialData, selectedData}) => {
return (
<div style={{height: "calc(80vh)"}}>
<DataGrid
style={{backgroundColor: theme.palette.background.paper}}
rows={data}
columns={columns}
initialState={{
@@ -119,7 +129,7 @@ const CustomSelectTable = ({initialData, selectedData}) => {
)
}
export function CallbacksTabsTaskMultipleDialog({onClose, callback, me}) {
export function CallbacksTabsTaskMultipleDialog({onClose, callback}) {
const mountedRef = React.useRef(true);
const [selectedToken, setSelectedToken] = React.useState({});
const [openTaskingButton, setOpenTaskingButton] = React.useState(false);
@@ -139,8 +149,8 @@ export function CallbacksTabsTaskMultipleDialog({onClose, callback, me}) {
const submitTasking = () => {
//console.log("selectedFeature", selectedFeature)
if(selectedData.current.length === 0){
onClose();
console.log("selectedData.current.length === 0")
//onClose();
snackActions.warning("No callbacks selected");
return;
}
const callbacks = selectedData.current.map( c => c.display_id)
@@ -163,6 +173,11 @@ export function CallbacksTabsTaskMultipleDialog({onClose, callback, me}) {
}
const onSubmitCommandLine = (message, cmd, parsed, force_parsed_popup, cmdGroupNames, previousTaskingLocation) => {
//console.log(message, cmd, parsed);
if(selectedData.current.length === 0){
//onClose();
snackActions.warning("No callbacks selected");
return;
}
let params = message.split(" ");
delete params[0];
params = params.join(" ").trim();
@@ -272,7 +287,6 @@ export function CallbacksTabsTaskMultipleDialog({onClose, callback, me}) {
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Task Multiple {callback.payload.payloadtype.name} Callbacks at Once</DialogTitle>
<CustomSelectTable initialData={initialData}
selectedData={selectedData} />
<Grid item xs={12} >
@@ -286,7 +286,13 @@ export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, pa
}
if(cmd.commandparameters.length === 0){
// if there are no parameters, just send whatever the user types along
onCreateTask({callback_id: tabInfo.displayID, command: cmd.cmd, params: params, parameter_group_name: "Default", tasking_location: newTaskingLocation});
onCreateTask({callback_id: tabInfo.displayID,
command: cmd.cmd,
params: params,
parameter_group_name: "Default",
tasking_location: newTaskingLocation,
payload_type: cmd.payloadtype?.name,
});
}else{
// check if there's a "file" component that needs to be displayed
const fileParamExists = cmd.commandparameters.find(param => {
@@ -333,20 +339,29 @@ export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, pa
params: JSON.stringify(parsed),
tasking_location: newTaskingLocation,
original_params: params,
parameter_group_name: cmdGroupNames[0]});
parameter_group_name: cmdGroupNames[0],
payload_type: cmd.payloadtype?.name,
});
}
}
}
const submitParametersDialog = (cmd, parameters, files, selectedParameterGroup) => {
const submitParametersDialog = (cmd, parameters, files, selectedParameterGroup, payload_type) => {
setOpenParametersDialog(false);
onCreateTask({callback_id: tabInfo.displayID, command: cmd, params: parameters, files: files, tasking_location: "modal", parameter_group_name: selectedParameterGroup});
onCreateTask({callback_id: tabInfo.displayID,
command: cmd,
params: parameters,
files: files,
tasking_location: "modal",
parameter_group_name: selectedParameterGroup,
payload_type: payload_type
});
}
const onCreateTask = ({callback_id, command, params, files, tasking_location, original_params, parameter_group_name}) => {
const onCreateTask = ({callback_id, command, params, files, tasking_location, original_params, parameter_group_name, payload_type}) => {
//console.log(selectedToken)
if(selectedToken.token_id !== undefined){
createTask({variables: {callback_id, command, params, files, token_id: selectedToken.token_id, tasking_location, original_params, parameter_group_name}});
createTask({variables: {callback_id, command, params, files, token_id: selectedToken.token_id, tasking_location, original_params, parameter_group_name, payload_type}});
}else{
createTask({variables: {callback_id, command, params, files, tasking_location, original_params, parameter_group_name}});
createTask({variables: {callback_id, command, params, files, tasking_location, original_params, parameter_group_name, payload_type}});
}
}
const onSubmitFilter = (newFilter) => {
@@ -363,18 +378,19 @@ export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, pa
}
return (
<MythicTabPanel index={index} value={value} >
{!fetchedAllTasks &&
<MythicStyledTooltip title="Fetch Older Tasks">
<IconButton
onClick={loadMoreTasks}
variant="contained"
color="primary"
style={{marginLeft: "50%"}}
size="large"><AutorenewIcon /></IconButton>
</MythicStyledTooltip>}
{!fetched && <LinearProgress color="primary" thickness={2} style={{paddingTop: "5px"}}/>}
{loadingMore && <LinearProgress color="primary" thickness={2} style={{paddingTop: "5px"}}/>}
<div style={{overflowY: "auto", flexGrow: 1}}>
<div style={{overflowY: "auto", flexGrow: 1, width: "100%"}}>
{!fetchedAllTasks &&
<MythicStyledTooltip style={{marginLeft: "50%"}} title="Fetch Older Tasks">
<IconButton
onClick={loadMoreTasks}
variant="contained"
color="success"
size="large"><AutorenewIcon /></IconButton>
</MythicStyledTooltip>}
{
taskingData.task.map( (task) => (
<TaskDisplay key={"taskinteractdisplay" + task.id} me={me} task={task} command_id={task.command == null ? 0 : task.command.id}
@@ -155,7 +155,7 @@ export function CallbacksTabsTaskingFilterDialog(props) {
}
return (
<Root>
<DialogTitle id="form-dialog-title">Filter Which Tasks Are Visible</DialogTitle>
<DialogTitle id="form-dialog-title">Filter Which Tasks Are Visible in this Callback</DialogTitle>
<DialogContent dividers={true} style={{overflow: "hidden"}}>
<React.Fragment>
<FormControl className={classes.formControl}>
@@ -10,6 +10,8 @@ import { gql, useSubscription } from '@apollo/client';
import { snackActions } from '../../utilities/Snackbar';
import { meState } from '../../../cache';
import {useReactiveVar} from '@apollo/client';
import { validate as uuidValidate } from 'uuid';
import {MythicSelectFromListDialog} from "../../MythicComponents/MythicSelectFromListDialog";
const GetLoadedCommandsSubscription = gql`
subscription GetLoadedCommandsSubscription($callback_id: Int!){
@@ -19,9 +21,15 @@ subscription GetLoadedCommandsSubscription($callback_id: Int!){
cmd
id
attributes
payloadtype {
name
id
}
commandparameters {
id
parameter_type: type
choices
dynamic_query_function
required
name
ui_position
@@ -31,7 +39,6 @@ subscription GetLoadedCommandsSubscription($callback_id: Int!){
}
}
}
}
`;
export const subscriptionCallbackTokens = gql`
@@ -49,7 +56,7 @@ subscription subscriptionCallbackTokens ($callback_id: Int!){
`;
const subscriptionTask = gql`
subscription tasksSubscription($callback_id: Int!){
task(where: {callback_id: {_eq: $callback_id}, parent_task_id: {_is_null: true}}, order_by: {id: desc}){
task_stream(batch_size: 100, cursor: {initial_value: {timestamp: "1970-01-01"}}, where: {callback_id: {_eq: $callback_id}, parent_task_id: {_is_null: true}}){
id
original_params
display_params
@@ -80,7 +87,43 @@ const GetUpDownArrowName = (task) => {
return task.command_name + " " + task.original_params;
}
}
const GetCommandName = (task) => {
if(task.command){
return task.command.cmd;
} else {
return task.command_name;
}
}
const GetDefaultValueForType = (parameter_type) => {
switch(parameter_type){
case "string":
return "";
case "typedArray":
case "array":
return [];
case "number":
return 0;
case "boolean":
return true;
default:
return undefined;
}
}
const IsCLIPossibleParameterType = (parameter_type) => {
switch(parameter_type){
case "ChooseOne":
case "ChooseOneCustom":
case "Number":
case "Boolean":
case "Array":
case "TypedArray":
case "ChooseMultiple":
case "String":
return true;
default:
return false;
}
}
export function CallbacksTabsTaskingInputPreMemo(props){
const snackMessageStyles = {anchorOrigin:{vertical: "bottom", horizontal: "left"}, autoHideDuration: 2000, preventDuplicate: true, maxSnack: 1, style:{marginBottom: "50px"}};
const snackReverseSearchMessageStyles = {anchorOrigin:{vertical: "bottom", horizontal: "left"}, autoHideDuration: 1000, preventDuplicate: true, maxSnack: 1, style:{marginBottom: "100px"}};
@@ -95,13 +138,16 @@ export function CallbacksTabsTaskingInputPreMemo(props){
const [openFilterOptionsDialog, setOpenFilterOptionsDialog] = React.useState(false);
const [tokenOptions, setTokenOptions] = React.useState([]);
const [activeFiltering, setActiveFiltering] = React.useState(false);
const [unmodifiedHistoryValue, setUnmodifiedHistoryValue] = React.useState("parsed_cli");
const [reverseSearching, setReverseSearching] = React.useState(false);
const [reverseSearchString, setReverseSearchString] = React.useState('');
const [reverseSearchOptions, setReverseSearchOptions] = React.useState([]);
const [reverseSearchIndex, setReverseSearchIndex] = React.useState(-1);
const mountedRef = React.useRef(true);
const commandOptions = React.useRef([]);
const commandOptionsForcePopup = React.useRef(false);
const [openSelectCommandDialog, setOpenSelectCommandDialog] = React.useState(false);
const me = useReactiveVar(meState);
const forwardOrBackwardTabIndex = (event, currentIndex, options) => {
if(event.shiftKey){
@@ -131,8 +177,10 @@ export function CallbacksTabsTaskingInputPreMemo(props){
if(!mountedRef.current || !props.parentMountedRef.current){
return;
}
setTaskOptions(data.data.task);
const filteredOptions = data.data.task.filter( c => applyFilteringToTasks(c));
const newTasks = [...taskOptions, ...data.data.task_stream];
newTasks.sort((a,b) => a.id > b.id ? -1 : 1);
setTaskOptions(newTasks);
const filteredOptions = newTasks.filter( c => applyFilteringToTasks(c));
setFilteredTaskOptions(filteredOptions);
}
});
@@ -158,6 +206,21 @@ export function CallbacksTabsTaskingInputPreMemo(props){
//console.log("filter updated")
const filteredOptions = taskOptions.filter( c => applyFilteringToTasks(c));
setFilteredTaskOptions(filteredOptions);
let active = false;
if(props.filterOptions?.commandsList?.length > 0){
active = true;
} else if(props.filterOptions?.commentsFlag){
active = true;
} else if(props.filterOptions?.everythingButList?.length > 0){
active = true;
} else if(props.filterOptions?.hideErrors){
active = true;
} else if(props.filterOptions?.operatorsList?.length > 0){
active = true;
} else if(props.filterOptions?.parameterString !== ""){
active = true;
}
setActiveFiltering(active);
}, [props.filterOptions])
React.useEffect( () => {
return() => {
@@ -173,8 +236,8 @@ export function CallbacksTabsTaskingInputPreMemo(props){
if(props.filterOptions === undefined){
return true;
}
if(props.filterOptions["operatorsList"].length > 0){
if(!props.filterOptions["operatorsList"].includes(task.operator.username)){
if(props.filterOptions["operatorsList"]?.length > 0){
if(!props.filterOptions["operatorsList"]?.includes(task.operator.username)){
return false;
}
}
@@ -183,15 +246,15 @@ export function CallbacksTabsTaskingInputPreMemo(props){
return false;
}
}
if(props.filterOptions["commandsList"].length > 0){
if(props.filterOptions["commandsList"]?.length > 0){
// only show these commands
if(!props.filterOptions["commandsList"].includes(task.command_name)){
if(!props.filterOptions["commandsList"]?.includes(GetCommandName(task))){
return false;
}
}
if(props.filterOptions["everythingButList"].length > 0){
if(props.filterOptions["everythingButList"]?.length > 0){
if(task.command !== null){
if(props.filterOptions["everythingButList"].includes(task.command_name)){
if(props.filterOptions["everythingButList"]?.includes(GetCommandName(task))){
return false;
}
}
@@ -221,8 +284,9 @@ export function CallbacksTabsTaskingInputPreMemo(props){
setUnmodifiedHistoryValue("parsed_cli");
event.stopPropagation();
event.preventDefault();
return;
}
if(event.key === "Tab"){
if(event.key === "Tab" || (event.key === " " && event.shiftKey )){
// if we're still typing the command, we want this to cycle through possible matching commands
// if we have a command, this should cycle through parameter names that are required
event.stopPropagation();
@@ -263,50 +327,19 @@ export function CallbacksTabsTaskingInputPreMemo(props){
if(message[message.length -1] === " "){
// somebody hit tab after a parameter name or after a parameter value
const parsed = parseCommandLine(message, cmd);
if(parsed === undefined){
return;
}
const cmdGroupNames = determineCommandGroupName(cmd, parsed);
if(cmdGroupNames === undefined){
snackActions.warning("Two or more of the specified parameters can't be used together", snackMessageStyles);
return;
}
console.log("cmdGroupNames in tab", cmdGroupNames);
// look for required arguments that aren't present in our parsed dictionary
for(const [key, value] of Object.entries(parsed)){
if(key !== "_"){
if(value !== value || value === undefined){
snackActions.warning(key + " needs a valid value",snackMessageStyles);
return;
}
if(value === undefined){
// this means we parsed something and it's undefined, so we need a value
if(message.endsWith(" -" + key)){
//this value is undefined and it's the last one in the list, so we can potentially swap it out with another parameter
for(let i = 0; i < cmd.commandparameters.length; i++){
if(cmd.commandparameters[i]["required"] &&
!(cmd.commandparameters[i]["cli_name"] in parsed) &&
(cmdGroupNames.includes(cmd.commandparameters[i]["parameter_group_name"]) || cmdGroupNames.length === 0)){
const newMsg = message.trim().slice(0, -1 * key.length) + cmd.commandparameters[i]["cli_name"];
setMessage(newMsg);
return;
}
}
for(let i = 0; i < cmd.commandparameters.length; i++){
if(!cmd.commandparameters[i]["required"] &&
!(cmd.commandparameters[i]["cli_name"] in parsed) &&
(cmdGroupNames.includes(cmd.commandparameters[i]["parameter_group_name"]) || cmdGroupNames.length === 0)){
const newMsg = message.trim().slice(0, -1 * key.length) + cmd.commandparameters[i]["cli_name"];
setMessage(newMsg);
return;
}
}
}
snackActions.warning(key + " needs a value", snackMessageStyles);
return;
}
}
}
for(let i = 0; i < cmd.commandparameters.length; i++){
if(cmd.commandparameters[i]["required"] &&
!(cmd.commandparameters[i]["cli_name"] in parsed) &&
IsCLIPossibleParameterType(cmd.commandparameters[i]["parameter_type"]) &&
(cmdGroupNames.includes(cmd.commandparameters[i]["parameter_group_name"]) || cmdGroupNames.length === 0)){
const newMsg = message.trim() + " -" + cmd.commandparameters[i]["cli_name"];
setMessage(newMsg);
@@ -316,6 +349,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
for(let i = 0; i < cmd.commandparameters.length; i++){
if(!cmd.commandparameters[i]["required"] &&
!(cmd.commandparameters[i]["cli_name"] in parsed) &&
IsCLIPossibleParameterType(cmd.commandparameters[i]["parameter_type"]) &&
(cmdGroupNames.includes(cmd.commandparameters[i]["parameter_group_name"]) || cmdGroupNames.length === 0)){
const newMsg = message.trim() + " -" + cmd.commandparameters[i]["cli_name"];
setMessage(newMsg);
@@ -351,6 +385,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
let exactMatch = cmd.commandparameters.find(cur =>
cmdGroupNames.includes(cur.parameter_group_name) &&
cur.cli_name === lastFlag.slice(1) &&
IsCLIPossibleParameterType(cur.parameter_type) &&
!(cur.cli_name in parsed)
);
let paramOptions = [];
@@ -360,6 +395,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
paramOptions = cmd.commandparameters.reduce( (prev, cur) => {
if(cmdGroupNames.includes(cur.parameter_group_name) &&
cur.cli_name !== lastFlag.slice(1) &&
IsCLIPossibleParameterType(cur.parameter_type) &&
!(cur.cli_name in parsed)){
return [...prev, cur.cli_name];
}else{
@@ -372,6 +408,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
paramOptions = cmd.commandparameters.reduce( (prev, cur) => {
if(cmdGroupNames.includes(cur.parameter_group_name) &&
cur.cli_name.toLowerCase().startsWith(lastFlag.slice(1).toLocaleLowerCase()) &&
IsCLIPossibleParameterType(cur.parameter_type) &&
!(cur.cli_name in parsed)){
return [...prev, cur.cli_name];
}else{
@@ -453,9 +490,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
}
setTaskOptionsIndex(newIndex);
setMessage(GetUpDownArrowName(filteredTaskOptions[newIndex]));
//setMessage(filteredTaskOptions[newIndex].command_name + " " + filteredTaskOptions[newIndex].original_params);
setUnmodifiedHistoryValue(filteredTaskOptions[newIndex].tasking_location);
//setMessage(taskOptions[newIndex].command_name + " " + taskOptions[newIndex].display_params);
}
}else if(event.key === "ArrowDown"){
if(filteredTaskOptions.length === 0){
@@ -468,9 +503,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
}
setTaskOptionsIndex(newIndex);
setMessage(GetUpDownArrowName(filteredTaskOptions[newIndex]));
//setMessage(filteredTaskOptions[newIndex].command_name + " " + filteredTaskOptions[newIndex].original_params);
setUnmodifiedHistoryValue(filteredTaskOptions[newIndex].tasking_location);
//setMessage(taskOptions[newIndex].command_name + " " + taskOptions[newIndex].display_params);
}
}else if(!event.shiftKey){
setTabOptions([]);
@@ -608,9 +641,14 @@ export function CallbacksTabsTaskingInputPreMemo(props){
let arrayArgs = [];
let typedArrayArgs = [];
let numberArgs = [];
let fileArgs = [];
let complexArgs = [];
let allCLINames = [];
for(let i = 0; i < cmd.commandparameters.length; i++){
allCLINames.push("-" + cmd.commandparameters[i].cli_name);
switch(cmd.commandparameters[i].parameter_type){
case "Choice":
case "ChooseOne":
case "ChooseOneCustom":
case "String":
stringArgs.push("-" + cmd.commandparameters[i].cli_name);
break;
@@ -621,14 +659,17 @@ export function CallbacksTabsTaskingInputPreMemo(props){
booleanArgs.push("-" + cmd.commandparameters[i].cli_name);
break;
case "Array":
case "ChoiceMultiple":
case "ChooseMultiple":
arrayArgs.push("-" + cmd.commandparameters[i].cli_name);
break;
case "TypedArray":
typedArrayArgs.push("-" + cmd.commandparameters[i].cli_name);
break;
case "File":
fileArgs.push("-" + cmd.commandparameters[i].cli_name);
break;
default:
stringArgs.push("-" + cmd.commandparameters[i].cli_name);
complexArgs.push("-" + cmd.commandparameters[i].cli_name);
}
}
let result = {"_": []};
@@ -636,7 +677,6 @@ export function CallbacksTabsTaskingInputPreMemo(props){
let current_argument_type = "";
for(let i = 0; i < argv.length; i++){
let value = argv[i];
//console.log(argv[i], current_argument, current_argument_type)
if(current_argument === ""){
// not currently processing the value for an argument
// check to see if this is the start of a new argument
@@ -644,34 +684,80 @@ export function CallbacksTabsTaskingInputPreMemo(props){
if(stringArgs.includes(value)){
current_argument_type = "string";
current_argument = value;
if(i === argv.length-1){
// special case where somebody did -flag at the end of the command
result[value.slice(1)] = GetDefaultValueForType(current_argument_type);
}
}else if(booleanArgs.includes(value)){
current_argument_type = "boolean";
if(i === argv.length-1){
// special case where somebody did -flag at the end of the command
result[value.slice(1)] = true;
result[value.slice(1)] = GetDefaultValueForType(current_argument_type);
}
current_argument = value;
}else if(arrayArgs.includes(value)) {
current_argument_type = "array";
current_argument = value;
if(i === argv.length-1){
// special case where somebody did -flag at the end of the command
result[value.slice(1)] = GetDefaultValueForType(current_argument_type);
}
} else if(typedArrayArgs.includes(value)){
current_argument_type = "typedArray";
current_argument = value;
}else if(numberArgs.includes(value)){
if(i === argv.length-1){
// special case where somebody did -flag at the end of the command
result[value.slice(1)] = GetDefaultValueForType(current_argument_type);
}
}else if(numberArgs.includes(value)) {
current_argument_type = "number";
current_argument = value;
if (i === argv.length - 1) {
// special case where somebody did -flag at the end of the command
result[value.slice(1)] = GetDefaultValueForType(current_argument_type);
}
}else if(fileArgs.includes(value)) {
current_argument_type = "file";
current_argument = value;
if (i === argv.length - 1) {
// special case where somebody did -flag at the end of the command
result[value.slice(1)] = GetDefaultValueForType(current_argument_type);
}
}else if(complexArgs.includes(value)){
current_argument_type = "complex";
current_argument = value;
if (i === argv.length - 1) {
// special case where somebody did -flag at the end of the command
result[value.slice(1)] = GetDefaultValueForType(current_argument_type);
}
} else {
// we don't have this as a named argument, so we'll process it as a positional one
result["_"].push(value);
}
} else {
// we have a named argument that we just saw, so interpret this as that argument's value
if(allCLINames.includes(value)){
result[current_argument.slice(1)] = GetDefaultValueForType(current_argument_type);
current_argument = "";
current_argument_type = "";
i -= 1;
continue;
}
switch(current_argument_type){
case "string":
result[current_argument.slice(1)] = value;
current_argument = "";
current_argument_type = "";
break;
case "file":
if(uuidValidate(value)){
result[current_argument.slice(1)] = value;
current_argument = "";
current_argument_type = "";
break;
}
snackActions.warning("File type value must be UUID of uploaded file: " + value, snackMessageStyles);
return undefined;
case "boolean":
if(["false", "true"].includes(value.toLowerCase())){
if(value.toLowerCase() === "false"){
@@ -747,10 +833,18 @@ export function CallbacksTabsTaskingInputPreMemo(props){
}
}
break;
case "complex":
try{
result[current_argument.slice(1)] = JSON.parse(value);
}catch(error){
result[current_argument.slice(1)] = value;
}
current_argument = "";
current_argument_type = "";
break;
default:
break;
}
}
};
return result;
@@ -816,6 +910,9 @@ export function CallbacksTabsTaskingInputPreMemo(props){
if(cmd.commandparameters.length === 0){
return [];
}
if(!parsed){
return [];
}
let cmdGroupOptions = cmd.commandparameters.reduce( (prev, cur) => {
if(prev.includes(cur.parameter_group_name)){
return [...prev];
@@ -1003,16 +1100,13 @@ export function CallbacksTabsTaskingInputPreMemo(props){
return parsedCopy;
}
const onSubmitCommandLine = (evt, force_parsed_popup) => {
evt.preventDefault();
evt.stopPropagation();
//console.log("onSubmitCommandLine", evt, message);
let splitMessage = message.trim().split(" ");
let cmd = loadedOptions.find( l => l.cmd === splitMessage[0]);
if(cmd === undefined){
snackActions.warning("Unknown (or not loaded) command", snackMessageStyles);
const processCommandAndCommandLine = (cmd) => {
if(commandOptionsForcePopup.current && cmd.commandparameters.length === 0){
snackActions.info("No defined parameters for " +
cmd?.cmd + "( " + cmd?.payloadtype?.name + "), so no modal available");
return;
}
let splitMessage = message.trim().split(" ");
let cmdGroupName = ["Default"];
let parsedWithPositionalParameters = {};
let params = splitMessage.slice(1).join(" ");
@@ -1031,7 +1125,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
}catch(error){
snackActions.warning("Failed to parse modified JSON value", snackMessageStyles);
return;
}
}
}else{
let parsed = parseCommandLine(params, cmd);
//console.log("result of parseCommandLine", parsed, !parsed)
@@ -1066,18 +1160,18 @@ export function CallbacksTabsTaskingInputPreMemo(props){
snackActions.warning("Two or more of the specified parameters can't be used together", snackMessageStyles);
return;
}else if(cmdGroupName.length > 1){
if(Boolean(force_parsed_popup)){
props.onSubmitCommandLine(message, cmd, parsedWithPositionalParameters, Boolean(force_parsed_popup), cmdGroupName, unmodifiedHistoryValue);
if(Boolean(commandOptionsForcePopup.current)){
props.onSubmitCommandLine(message, cmd, parsedWithPositionalParameters, Boolean(commandOptionsForcePopup.current), cmdGroupName, unmodifiedHistoryValue);
}else{
if(cmdGroupName.includes("Default")){
props.onSubmitCommandLine(message, cmd, parsedWithPositionalParameters, Boolean(force_parsed_popup), ["Default"], unmodifiedHistoryValue);
props.onSubmitCommandLine(message, cmd, parsedWithPositionalParameters, Boolean(commandOptionsForcePopup.current), ["Default"], unmodifiedHistoryValue);
}else{
let simplifiedGroupName = simplifyGroupNameChoices(cmdGroupName, cmd, parsedWithPositionalParameters)
if(simplifiedGroupName === "" ){
snackActions.warning("Passed arguments are ambiguous, use shift+enter for modal or provide more parameters", snackMessageStyles);
return;
} else {
props.onSubmitCommandLine(message, cmd, parsedWithPositionalParameters, Boolean(force_parsed_popup), [simplifiedGroupName], unmodifiedHistoryValue);
props.onSubmitCommandLine(message, cmd, parsedWithPositionalParameters, Boolean(commandOptionsForcePopup.current), [simplifiedGroupName], unmodifiedHistoryValue);
}
}
}
@@ -1089,13 +1183,33 @@ export function CallbacksTabsTaskingInputPreMemo(props){
return;
}
console.log("positional args added in:", parsedWithPositionalParameters);
props.onSubmitCommandLine(message, cmd, parsedWithPositionalParameters, Boolean(force_parsed_popup), cmdGroupName, unmodifiedHistoryValue);
props.onSubmitCommandLine(message, cmd, parsedWithPositionalParameters, Boolean(commandOptionsForcePopup.current), cmdGroupName, unmodifiedHistoryValue);
setMessage("");
setTaskOptionsIndex(-1);
setReverseSearchIndex(-1);
setReverseSearching(false);
setUnmodifiedHistoryValue("parsed_cli");
}
const onSubmitCommandLine = (evt, force_parsed_popup) => {
evt.preventDefault();
evt.stopPropagation();
//console.log("onSubmitCommandLine", evt, message);
let splitMessage = message.trim().split(" ");
let cmd = loadedOptions.filter( l => l.cmd === splitMessage[0]);
if(cmd === undefined || cmd.length === 0){
snackActions.warning("Unknown (or not loaded) command", snackMessageStyles);
return;
}
commandOptionsForcePopup.current = force_parsed_popup;
if(cmd.length === 1){
processCommandAndCommandLine(cmd[0], force_parsed_popup)
return;
}
// two or more commands share the same name, we need to disambiguate between them
cmd = cmd.map( c => {return {...c, display: `${c.cmd} (${c.payloadtype.name})`}});
commandOptions.current = cmd;
setOpenSelectCommandDialog(true);
}
const onClickFilter = () => {
setOpenFilterOptionsDialog(true);
}
@@ -1109,11 +1223,11 @@ export function CallbacksTabsTaskingInputPreMemo(props){
}
// need to do a reverse i search through taskOptions
const lowerCaseTextSearch = event.target.value.toLowerCase();
const matchingOptions = taskOptions.filter( x => (x.command_name + " " + x.original_params).toLowerCase().includes(lowerCaseTextSearch));
const matchingOptions = taskOptions.filter( x => (x?.command?.cmd + " " + x.original_params).toLowerCase().includes(lowerCaseTextSearch));
const filteredMatches = matchingOptions.filter( x => applyFilteringToTasks(x))
setReverseSearchOptions(filteredMatches);
if(filteredMatches.length > 0){
setMessage(filteredMatches[0].command_name + " " + filteredMatches[0].original_params);
setMessage(filteredMatches[0].command?.cmd + " " + filteredMatches[0].original_params);
}
}
const onReverseSearchKeyDown = (event) => {
@@ -1137,9 +1251,13 @@ export function CallbacksTabsTaskingInputPreMemo(props){
snackActions.warning("No matching options", snackReverseSearchMessageStyles);
return;
}else{
const newIndex = (reverseSearchIndex + 1) % reverseSearchOptions.length;
let newIndex = (reverseSearchIndex + 1);
if(newIndex > reverseSearchOptions.length -1){
newIndex = reverseSearchOptions.length -1;
}
setReverseSearchIndex(newIndex);
setMessage(reverseSearchOptions[newIndex].command_name + " " + reverseSearchOptions[newIndex].original_params);
setMessage(GetUpDownArrowName(reverseSearchOptions[newIndex]));
setUnmodifiedHistoryValue(reverseSearchOptions[newIndex].tasking_location);
}
}else if(event.key === "ArrowDown"){
// go down through the reverseSearchOptions by decrementing reverseSearchIndex
@@ -1148,12 +1266,13 @@ export function CallbacksTabsTaskingInputPreMemo(props){
snackActions.warning("No matching options", snackReverseSearchMessageStyles);
return;
}else{
let newIndex = (reverseSearchIndex - 1) % reverseSearchOptions.length;
let newIndex = (reverseSearchIndex - 1);
if(newIndex < 0){
newIndex = reverseSearchOptions.length - 1;
newIndex = 0;
}
setReverseSearchIndex(newIndex);
setMessage(reverseSearchOptions[newIndex].command_name + " " + reverseSearchOptions[newIndex].original_params);
setMessage(GetUpDownArrowName(reverseSearchOptions[newIndex]));
setUnmodifiedHistoryValue(reverseSearchOptions[newIndex].tasking_location);
}
}else if(event.key === "r" && event.ctrlKey){
//this means they typed ctrl+r, so they're wanting to do a reverse search for a command
@@ -1170,6 +1289,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
onKeyDown={onReverseSearchKeyDown}
onChange={handleReverseSearchInputChange}
size="small"
color={"secondary"}
autoFocus={true}
variant="outlined"
value={reverseSearchString}
@@ -1185,6 +1305,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
onKeyDown={onKeyDown}
onChange={handleInputChange}
size="small"
color={"secondary"}
variant="outlined"
multiline={true}
maxRows={15}
@@ -1197,13 +1318,13 @@ export function CallbacksTabsTaskingInputPreMemo(props){
endAdornment:
<React.Fragment>
<IconButton
color="primary"
color="info"
variant="contained"
onClick={onSubmitCommandLine}
size="large"><SendIcon/></IconButton>
{props.filterTasks &&
<IconButton
color="secondary"
color={activeFiltering ? "warning" : "secondary"}
variant="contained"
onClick={onClickFilter}
size="large"><TuneIcon/></IconButton>
@@ -1225,6 +1346,14 @@ export function CallbacksTabsTaskingInputPreMemo(props){
innerDialog={<CallbacksTabsTaskingFilterDialog filterCommandOptions={loadedOptions} onSubmit={props.onSubmitFilter} filterOptions={props.filterOptions} onClose={()=>{setOpenFilterOptionsDialog(false);}} />}
/>
}
{openSelectCommandDialog &&
<MythicDialog fullWidth={true} maxWidth="md" open={openSelectCommandDialog}
onClose={()=>{setOpenSelectCommandDialog(false)}}
innerDialog={<MythicSelectFromListDialog onClose={()=>{setOpenSelectCommandDialog(false);}}
onSubmit={processCommandAndCommandLine} options={commandOptions.current} title={"Select Command"}
action={"select"} identifier={"id"} display={"display"} />}
/>
}
</React.Fragment>
);
}
@@ -1,5 +1,5 @@
import React, {createContext} from 'react';
import {useSubscription, gql } from '@apollo/client';
import {useSubscription, gql, useQuery, useLazyQuery } from '@apollo/client';
import {CallbacksTable, CallbacksTableMaterialReactTable} from './CallbacksTable';
import {CallbacksGraph} from './CallbacksGraph';
import {useMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
@@ -9,10 +9,11 @@ export const CallbacksContext = createContext([]);
//
//callback(where: {active: {_eq: true}}){
const SUB_Callbacks = gql`
subscription CallbacksSubscription{
callback_stream(batch_size: 1000, cursor: {initial_value: {timestamp: "1970-01-01"}}) {
subscription CallbacksSubscription($fromNow: timestamp!){
callback_stream(batch_size: 1000, cursor: {initial_value: {timestamp: $fromNow}}) {
architecture
active
dead
display_id
description
domain
@@ -33,7 +34,50 @@ callback_stream(batch_size: 1000, cursor: {initial_value: {timestamp: "1970-01-0
user
agent_callback_id
operation_id
process_name
process_short_name
last_checkin
current_time
mythictree_groups
payload {
os
payloadtype {
name
agent_type
id
}
description
id
}
}
}
`;
const Query_Callbacks = gql`
query InitialActiveCallbacksQuery{
callback(where: {active: {_eq: true}}) {
architecture
active
dead
display_id
description
domain
external_ip
host
id
integrity_level
ip
locked
locked_operator {
username
id
}
extra_info
sleep_info
pid
os
user
agent_callback_id
operation_id
process_short_name
last_checkin
current_time
mythictree_groups
@@ -115,67 +159,91 @@ subscription CallbacksSubscription{
`;
export function CallbacksTop(props){
const me = props.me;
const [callbacks, setCallbacks] = React.useState([]);
const [callbackEdges, setCallbackEdges] = React.useState([]);
const callbacks = React.useRef([]);
const callbackEdges = React.useRef([]);
const fromNow = React.useRef(new Date());
const mountedRef = React.useRef(true);
useSubscription(SUB_Callbacks, {
variables: {fromNow: fromNow.current},
fetchPolicy: "no-cache",
onData: ({data}) => {
if(!mountedRef.current){
return;
}
if(!mountedRef.current){
return;
}
const updated = data.data.callback_stream.reduce( (prev, cur) => {
console.log(cur)
const updated = data.data.callback_stream.reduce( (prev, cur) => {
let existingIndex = prev.findIndex( (element, i, array) => element.id === cur.id);
if(existingIndex === -1){
// cur isn't in our current list of callbacks
if(cur.active){return [...prev, cur]}
}
if(!cur.active){
if(existingIndex !== -1){
prev.splice(existingIndex, 1);
return [...prev];
}
let existingIndex = prev.findIndex( (element, i, array) => element.id === cur.id);
if(existingIndex === -1){
// cur isn't in our current list of callbacks
if(cur.active){return [...prev, cur]}
}
if(!cur.active){
if(existingIndex !== -1){
prev.splice(existingIndex, 1);
return [...prev];
}
}
prev[existingIndex] = {...prev[existingIndex], ...cur};
return [...prev];
}, callbacks.current);
}
prev[existingIndex] = {...prev[existingIndex], ...cur};
return [...prev];
}, callbacks);
updated.sort( (a, b) => a.display_id > b.display_id ? -1 : 1);
setCallbacks(updated);
updated.sort( (a, b) => a.display_id > b.display_id ? -1 : 1);
callbacks.current = updated;
},
onError: ({data}) => {
console.log(data)
},
});
useQuery(Query_Callbacks, {
fetchPolicy: "no-cache",
onCompleted: (data) => {
const updated = data.callback.reduce( (prev, cur) => {
let existingIndex = prev.findIndex( (element, i, array) => element.id === cur.id);
if(existingIndex === -1){
// cur isn't in our current list of callbacks
if(cur.active){return [...prev, cur]}
}
if(!cur.active){
if(existingIndex !== -1){
prev.splice(existingIndex, 1);
return [...prev];
}
}
prev[existingIndex] = {...prev[existingIndex], ...cur};
return [...prev];
}, callbacks.current);
updated.sort( (a, b) => a.display_id > b.display_id ? -1 : 1);
callbacks.current = updated;
}
})
useSubscription(SUB_Edges, {
fetchPolicy: "network-only",
onData: ({data}) => {
if(!mountedRef.current){
return;
}
setCallbackEdges(data.data.callbackgraphedge)
callbackEdges.current = data.data.callbackgraphedge;
}
});
const onOpenTabLocal = React.useCallback( ({tabType, tabID, callbackID}) => {
for(let i = 0; i < callbacks.length; i++){
if(callbacks[i]["id"] === callbackID){
for(let i = 0; i < callbacks.current.length; i++){
if(callbacks.current[i]["id"] === callbackID){
const tabData = {tabID, tabType, callbackID,
displayID: callbacks[i]["display_id"],
payloadtype: callbacks[i]["payload"]["payloadtype"]["name"],
payloadtype_id: callbacks[i]["payload"]["payloadtype"]["id"],
operation_id: callbacks[i]["operation_id"],
payloadDescription: callbacks[i]["payload"]["description"],
callbackDescription: callbacks[i]["description"],
host: callbacks[i]["host"],
os: callbacks[i]["payload"]["os"]};
displayID: callbacks.current[i]["display_id"],
payloadtype: callbacks.current[i]["payload"]["payloadtype"]["name"],
payloadtype_id: callbacks.current[i]["payload"]["payloadtype"]["id"],
operation_id: callbacks.current[i]["operation_id"],
payloadDescription: callbacks.current[i]["payload"]["description"],
callbackDescription: callbacks.current[i]["description"],
host: callbacks.current[i]["host"],
os: callbacks.current[i]["payload"]["os"]};
props.onOpenTab(tabData);
}
}
}, [callbacks, props.onOpenTab]);
}, [callbacks.current, props.onOpenTab]);
React.useEffect( () => {
return() => {
mountedRef.current = false;
@@ -185,17 +253,15 @@ export function CallbacksTop(props){
const initialNewBrowserScriptTable = useMythicSetting({setting_name: "experiment-browserscripttable", default_value: "false"});
return (
<div style={{height: "100%", width: "100%"}}>
<CallbackGraphEdgesContext.Provider value={callbackEdges}>
<CallbackGraphEdgesContext.Provider value={callbackEdges.current}>
<OnOpenTabContext.Provider value={onOpenTabLocal}>
<CallbacksContext.Provider value={callbacks}>
<CallbacksContext.Provider value={callbacks.current}>
{props.topDisplay === "graph" ? (
<CallbacksGraph maxHeight={"100%"} />
<CallbacksGraph onOpenTab={onOpenTabLocal} maxHeight={"100%"} />
) : initialNewBrowserScriptTable ? (
<CallbacksTableMaterialReactTable onOpenTab={onOpenTabLocal}
parentMountedRef={mountedRef} me={me}/>
<CallbacksTableMaterialReactTable parentMountedRef={mountedRef} me={me}/>
) : (
<CallbacksTable onOpenTab={onOpenTabLocal}
parentMountedRef={mountedRef} me={me}/>
<CallbacksTable parentMountedRef={mountedRef} me={me}/>
)
}
</CallbacksContext.Provider>
@@ -12,7 +12,6 @@ import {useTheme} from '@mui/material/styles';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import Button from '@mui/material/Button';
import {ExpandedCallbackSideDetailsTable} from '../ExpandedCallback/ExpandedCallbackSideDetails';
import { toLocalTime } from '../../utilities/Time';
import {PayloadsTableRowBuildProcessPerStep} from '../Payloads/PayloadsTableRowBuildProgress';
@@ -25,6 +24,9 @@ import { snackActions } from '../../utilities/Snackbar';
import Box from '@mui/material/Box';
import Dialog from '@mui/material/Dialog';
import {ParseForDisplay} from "../Payloads/DetailedPayloadTable";
import {Button, Link, IconButton} from '@mui/material';
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
import MenuBookIcon from '@mui/icons-material/MenuBook';
const GET_Payload_Details = gql`
query GetCallbackDetails($callback_id: Int!) {
@@ -44,6 +46,23 @@ query GetCallbackDetails($callback_id: Int!) {
md5
sha1
}
operator {
username
}
eventstepinstance{
id
eventgroupinstance {
id
eventgroup {
id
name
}
}
eventstep {
id
name
}
}
payload_build_steps(order_by: {step_number: asc}) {
step_name
step_number
@@ -88,6 +107,9 @@ query GetCallbackDetails($callback_id: Int!) {
cmd
id
version
payloadtype {
name
}
}
}
architecture
@@ -221,7 +243,7 @@ export function DetailedCallbackTable(props){
onCompleted: data => {
const commandState = data.callback_by_pk.loadedcommands.map( (c) =>
{
return {cmd: c.command.cmd, mythic: c.command.version, payload: c.version}
return {cmd: c.command.cmd, mythic: c.command.version, payload: c.version, payload_type: c.command.payloadtype.name}
}).sort((a,b) => (a.cmd > b.cmd) ? 1: ((b.cmd > a.cmd) ? -1 : 0));
setCommands(commandState);
const buildParametersState = data.callback_by_pk.payload.buildparameterinstances.map( (b) =>
@@ -324,6 +346,28 @@ export function DetailedCallbackTable(props){
<TableCell>MD5</TableCell>
<TableCell>{data.callback_by_pk.payload.filemetum.md5}</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Created By</TableCell>
<TableCell>{data.callback_by_pk.payload?.operator?.username}</TableCell>
</TableRow>
{data.callback_by_pk.payload?.eventstepinstance &&
<>
<TableRow hover>
<TableCell>Generated via Eventing</TableCell>
<TableCell>
<Link color="textPrimary" underline="always"
href={"/new/eventing?eventgroup=" +
data.callback_by_pk.payload?.eventstepinstance?.eventgroupinstance?.eventgroup.id +
"&eventgroupinstance=" +
data.callback_by_pk.payload?.eventstepinstance?.eventgroupinstance?.id
}>
{data.callback_by_pk.payload?.eventstepinstance?.eventgroupinstance?.eventgroup?.name} -
{data.callback_by_pk.payload?.eventstepinstance?.eventstep?.name}
</Link>
</TableCell>
</TableRow>
</>
}
</TableBody>
</Table>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
@@ -353,10 +397,10 @@ export function DetailedCallbackTable(props){
): (cmd.value)
)
}
{cmd.enc_key === null ? (null) : (<React.Fragment>
{cmd.enc_key === null ? null : (<React.Fragment>
<br/><b>Encryption Key: </b> {cmd.enc_key}
</React.Fragment>) }
{cmd.dec_key === null ? (null) : (<React.Fragment>
{cmd.dec_key === null ? null : (<React.Fragment>
<br/><b>Decryption Key: </b> {cmd.dec_key}
</React.Fragment>) }
</TableCell>
@@ -417,10 +461,10 @@ export function DetailedCallbackTable(props){
<TableCell>{cmd.description}</TableCell>
<TableCell>
<ParseForDisplay cmd={cmd} />
{cmd.enc_key === null ? (null) : (<React.Fragment>
{cmd.enc_key === null ? null : (<React.Fragment>
<br/><b>Encryption Key: </b> {cmd.enc_key}
</React.Fragment>) }
{cmd.dec_key === null ? (null) : (<React.Fragment>
{cmd.dec_key === null ? null : (<React.Fragment>
<br/><b>Decryption Key: </b> {cmd.dec_key}
</React.Fragment>) }
</TableCell>
@@ -441,6 +485,7 @@ export function DetailedCallbackTable(props){
<Table size="small" aria-label="details" style={{"overflowWrap": "break-word"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "40px"}}>Payload</TableCell>
<TableCell>Command Name</TableCell>
<TableCell>Mythic Version</TableCell>
<TableCell>Loaded Version</TableCell>
@@ -450,22 +495,27 @@ export function DetailedCallbackTable(props){
<TableBody>
{
commands.map( (cmd) => (
<TableRow key={cmd.cmd + data.callback_by_pk.payload.id} hover>
<TableCell>{cmd.cmd}</TableCell>
<TableRow key={cmd.cmd + cmd.payload_type} hover>
<TableCell>
<MythicAgentSVGIcon payload_type={cmd.payload_type} style={{width: "40px"}}/>
</TableCell>
<TableCell>{cmd.cmd} ({cmd.payload_type})</TableCell>
<TableCell>{cmd.mythic}</TableCell>
<TableCell>{cmd.payload}</TableCell>
<TableCell>
<Button variant="contained" color="primary" target="_blank"
href={"/docs/agents/" + data.callback_by_pk.payload.payloadtype.name + "/commands/" + cmd.cmd}>Docs</Button>
<IconButton variant="contained" target="_blank"
href={"/docs/agents/" + cmd.payload_type + "/commands/" + cmd.cmd}>
<MenuBookIcon />
</IconButton>
</TableCell>
</TableRow>
))
}
{openAddRemoveCommandsDialog &&
<MythicDialog fullWidth={true} maxWidth="md" open={openAddRemoveCommandsDialog}
onClose={()=>{setOpenAddRemoveCommandsDialog(false);}}
innerDialog={<AddRemoveCallbackCommandsDialog callback_id={props.callback_id}
innerDialog={<AddRemoveCallbackCommandsDialog
callback_id={props.callback_id}
display_id={props.display_id} onClose={()=>{setOpenAddRemoveCommandsDialog(false);}} onSubmit={addRemoveCommandsSubmit} />}
/>
}
@@ -1,6 +1,5 @@
import React, {useEffect} from 'react';
import {gql, useLazyQuery, useReactiveVar, useSubscription} from '@apollo/client';
import {meState} from '../../../cache';
import {gql, useLazyQuery, useSubscription} from '@apollo/client';
import {snackActions} from '../../utilities/Snackbar';
import {ResponseDisplayScreenshot} from './ResponseDisplayScreenshot';
import {ResponseDisplayPlaintext} from './ResponseDisplayPlaintext';
@@ -24,7 +23,7 @@ const subResponsesStream = gql`
subscription subResponsesStream($task_id: Int!){
response_stream(
batch_size: 50,
cursor: {initial_value: {id: 0}},
cursor: {initial_value: {timestamp: "1970-01-01"}},
where: {task_id: {_eq: $task_id} }
){
id
@@ -62,8 +61,8 @@ query subResponsesQuery($task_id: Int!, $search: String!) {
}
}`;
const taskScript = gql`
query getBrowserScriptsQuery($command_id: Int!, $operator_id: Int!){
browserscript(where: {active: {_eq: true}, command_id: {_eq: $command_id}, for_new_ui: {_eq: true}, operator_id: {_eq: $operator_id}}) {
query getBrowserScriptsQuery($command_id: Int!){
browserscript(where: {active: {_eq: true}, command_id: {_eq: $command_id}, for_new_ui: {_eq: true}}) {
script
id
}
@@ -200,9 +199,12 @@ const NonInteractiveResponseDisplay = (props) => {
return;
}
// we still have some room to view more, but only room for initialResponseStreamLimit - totalFetched.current
const currentIDS = rawResponses.map( r => r.id);
const newerResponses = data.data.response_stream.reduce( (prev, cur) => {
if(currentIDS.includes(cur.id)){return prev}
let prevIndex = prev.findIndex( (v,i,a) => v.id === cur.id);
if(prevIndex >= 0){
prev[prevIndex] = {...cur, response: b64DecodeUnicode(cur.response)};
return prev;
}
return [...prev, {...cur, response: b64DecodeUnicode(cur.response)}]
}, rawResponses);
// sort them to make sure we're still in order
@@ -331,9 +333,12 @@ export const NonInteractiveResponseDisplayConsole = (props) => {
setRawResponses(responseArray);
} else {
// we still have some room to view more, but only room for initialResponseStreamLimit - totalFetched.current
const currentIDS = rawResponses.map( r => r.id);
const newerResponses = data.data.response_stream.reduce( (prev, cur) => {
if(currentIDS.includes(cur.id)){return prev}
let prevIndex = prev.findIndex( (v,i,a) => v.id === cur.id);
if(prevIndex >= 0){
prev[prevIndex] = {...cur, response: b64DecodeUnicode(cur.response)};
return prev;
}
return [...prev, {...cur, response: b64DecodeUnicode(cur.response)}]
}, rawResponses);
// sort them to make sure we're still in order
@@ -435,7 +440,6 @@ const ResponseDisplayComponent = ({rawResponses, viewBrowserScript, output, comm
const [loadingBrowserScript, setLoadingBrowserScript] = React.useState(true);
const useNewBrowserScriptTable = useMythicSetting({setting_name: "experiment-browserscripttable", default_value: "false"});
const script = React.useRef();
const me = useReactiveVar(meState);
useEffect( () => {
if(script.current !== undefined){
try{
@@ -464,22 +468,25 @@ const ResponseDisplayComponent = ({rawResponses, viewBrowserScript, output, comm
if(useNewBrowserScriptTable){return copied}
if(scriptData["table"] !== undefined){
if(scriptData["table"].length > 0){
const tableUpdates = scriptData.table.map( t => {
const filteredRows = t.rows.filter( r => {
copied["table"] = scriptData.table.map(t => {
const filteredRows = t.rows.filter(r => {
let foundMatch = false;
for (const entry of Object.values(r)) {
if(entry["plaintext"] !== undefined){
if(String(entry["plaintext"]).toLowerCase().includes(search)){foundMatch = true;}
if (entry["plaintext"] !== undefined) {
if (String(entry["plaintext"]).toLowerCase().includes(search)) {
foundMatch = true;
}
}
if(entry["button"] !== undefined && entry["button"]["value"] !== undefined){
if(JSON.stringify(entry["button"]["value"]).includes(search)){foundMatch = true;}
if (entry["button"] !== undefined && entry["button"]["value"] !== undefined) {
if (JSON.stringify(entry["button"]["value"]).includes(search)) {
foundMatch = true;
}
}
}
return foundMatch;
});
return {...t, rows: filteredRows};
});
copied["table"] = tableUpdates;
}
}
@@ -488,18 +495,17 @@ const ResponseDisplayComponent = ({rawResponses, viewBrowserScript, output, comm
useEffect( () => {
if(script.current === undefined){
setViewBrowserScript(false);
}else{
setViewBrowserScript(viewBrowserScript);
if(viewBrowserScript){
try{
const rawResponseData = rawResponses.map(c => c.response);
let scriptTaskData = JSON.parse(JSON.stringify(task));
let res = script.current(scriptTaskData, rawResponseData);
setBrowserScriptData(filterOutput(res));
}catch(error){
setViewBrowserScript(false);
}
return;
}
setViewBrowserScript(viewBrowserScript);
if(viewBrowserScript){
try{
const rawResponseData = rawResponses.map(c => c.response);
let scriptTaskData = JSON.parse(JSON.stringify(task));
let res = script.current(scriptTaskData, rawResponseData);
setBrowserScriptData(filterOutput(res));
}catch(error){
setViewBrowserScript(false);
}
}
}, [viewBrowserScript]);
@@ -536,7 +542,7 @@ const ResponseDisplayComponent = ({rawResponses, viewBrowserScript, output, comm
useEffect( () => {
if(command_id !== undefined){
setLoadingBrowserScript(true);
fetchScripts({variables: {command_id: command_id, operator_id: me.user.user_id}});
fetchScripts({variables: {command_id: command_id}});
}
}, [command_id, task.id]);
if(loadingBrowserScript){
@@ -21,6 +21,20 @@ import 'ace-builds/src-noconflict/mode-java';
import 'ace-builds/src-noconflict/mode-javascript';
import 'ace-builds/src-noconflict/theme-monokai';
import 'ace-builds/src-noconflict/theme-xcode';
import 'ace-builds/src-noconflict/mode-yaml';
import 'ace-builds/src-noconflict/mode-toml';
import 'ace-builds/src-noconflict/mode-swift';
import 'ace-builds/src-noconflict/mode-sql';
import 'ace-builds/src-noconflict/mode-rust';
import 'ace-builds/src-noconflict/mode-powershell';
import 'ace-builds/src-noconflict/mode-pgsql';
import 'ace-builds/src-noconflict/mode-perl';
import 'ace-builds/src-noconflict/mode-php';
import 'ace-builds/src-noconflict/mode-objectivec';
import 'ace-builds/src-noconflict/mode-nginx';
import 'ace-builds/src-noconflict/mode-makefile';
import 'ace-builds/src-noconflict/mode-kotlin';
import 'ace-builds/src-noconflict/mode-dockerfile';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
@@ -33,7 +47,31 @@ const textExtensionTypes = ["txt", "ps1", "php", "json", "yml", "yaml", "config"
"pem", "boto", "zsh_profile", "pub", "python_history", "sqlite_history", "viminfo", "zprofile", "zshrc",
"history", "historynew"
];
const knownTextFiles = ["config", "credentials", "known_hosts", "config_default", "id_rsa"];
const textExtensionTypesToSyntax = {
"json": "json",
"cs": "csharp",
"go": "golang",
"md": "markdown",
"MD": "markdown",
"rb": "ruby",
"py": "python",
"java": "java",
"js": "javascript",
"yml": "yaml",
"yaml": "yaml",
"toml": "toml",
"swift": "swift",
"psql": "sql",
"rs": "rust",
"ps1": "powershell",
"prl": "perl",
"php": "php",
"m": "objectivec",
"mm": "objectivec",
"Dockerfile": "Dockerfile",
"Makefile": "Makefile"
}
const knownTextFiles = ["config", "credentials", "known_hosts", "config_default", "id_rsa", "Dockerfile", "Makefile"];
const imgExtensionTypes = ["png", "jpg", "gif", "jpeg", "pdf"];
const mimeType = (path) => {
if(!path){return undefined}
@@ -132,7 +170,7 @@ export const DisplayMedia = ({agent_file_id, filename, expand, task}) => {
if(fileData.display_type === "text"){
return (
<div style={{height: "100%", minHeight: "200px", width: "100%"}} >
<DisplayText agent_file_id={agent_file_id} expand={expand} />
<DisplayText agent_file_id={agent_file_id} expand={expand} filename={filename} />
</div>
)
@@ -140,7 +178,7 @@ export const DisplayMedia = ({agent_file_id, filename, expand, task}) => {
return null;
}
const MaxRenderSize = 2000000;
const DisplayText = ({agent_file_id, expand}) => {
const DisplayText = ({agent_file_id, expand, filename}) => {
const theme = useTheme();
const [mode, setMode] = React.useState("html");
const [content, setContent] = React.useState("");
@@ -169,6 +207,19 @@ const DisplayText = ({agent_file_id, expand}) => {
}
console.log("There was an error!", error);
});
if(filename){
let extension = filename.split(".");
if(extension.length > 0){
extension = extension[extension.length - 1];
if(textExtensionTypesToSyntax[extension]){
setMode(textExtensionTypesToSyntax[extension]);
}
} else {
if(textExtensionTypesToSyntax[filename]){
setMode(textExtensionTypesToSyntax[filename]);
}
}
}
}, []);
const onChangeMode = (event) => {
setMode(event.target.value);
@@ -95,7 +95,7 @@ export const ResponseDisplayPlaintext = (props) =>{
</MythicStyledTooltip>
</div>
}
<div style={{height: "1px", width: "100%", display: "flex", zIndex: 1, justifyContent: "space-around", backgroundColor: theme.palette.secondary.main}}>
<div style={{height: "1px", width: "100%", display: "flex", zIndex: 1, backgroundColor: theme.palette.secondary.main}}>
{showOptions &&
<UnfoldLessIcon onClick={onChangeShowOptions} style={{cursor: "pointer", position: "relative", top: "-8px"}} />
}
@@ -25,33 +25,34 @@ export function ResponseDisplayScreenshotModal({onClose, images, startIndex}) {
return (
<React.Fragment>
<DialogContent dividers={true} style={{padding: 0}}>
<div style={{height: "calc(80vh)", display: zoom ? "" : "flex", alignItems: zoom ? "" : "center" , background: theme.palette.mode === "dark" ? "rgb(44, 52, 60)" : "grey"}}>
<div style={{height: "calc(80vh)", display: zoom ? "" : "flex", alignItems: zoom ? "" : "flex-start" , background: theme.palette.mode === "dark" ? "rgb(44, 52, 60)" : "grey"}}>
<img onClick={toggleZoom} src={"/api/v1.4/files/screencaptures/" + images[activeStep]} style={{width: zoom ? "" : "100%", cursor: zoom ? "zoom-out" : "zoom-in", overflow: "auto"}} />
</div>
</DialogContent>
<MobileStepper
variant="text"
steps={maxSteps}
position="static"
activeStep={activeStep}
nextButton={
<Button
size="small"
onClick={handleNext}
disabled={activeStep === maxSteps - 1}
>
Next
{<KeyboardArrowRight />}
</Button>
}
backButton={
<Button size="small" onClick={handleBack} disabled={activeStep === 0}>
{<KeyboardArrowLeft />}
Back
</Button>
}
variant="text"
steps={maxSteps}
position="static"
activeStep={activeStep}
nextButton={
<Button
size="small"
onClick={handleNext}
disabled={activeStep === maxSteps - 1}
>
Next
{<KeyboardArrowRight />}
</Button>
}
backButton={
<Button size="small" onClick={handleBack} disabled={activeStep === 0}>
{<KeyboardArrowLeft />}
Back
</Button>
}
/>
<DialogActions>
<Button variant="contained" onClick={onClose} color="primary">
Close
</Button>
@@ -253,11 +253,11 @@ const ResponseDisplayTableActionCell = ({cellData, callback_id, rowData}) => {
case "dictionary":
return (
<React.Fragment>
<MythicStyledTooltip title={cellData?.button?.hoverText || " "} >
<MythicStyledTooltip title={cellData?.button?.hoverText || "View Data"} >
<Button size="small" variant="contained" color="primary"
onClick={() => setOpenButton(true)} disabled={cellData?.button?.disabled || false}
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.startIconColor || ""}}/> : null}
>{cellData?.button?.name || " "}</Button>
>{cellData?.button?.name || "View Dictionary Data"}</Button>
</MythicStyledTooltip>
{openButton &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openButton}
@@ -271,11 +271,11 @@ const ResponseDisplayTableActionCell = ({cellData, callback_id, rowData}) => {
case "string":
return (
<React.Fragment>
<MythicStyledTooltip title={cellData?.button?.hoverText || " "} >
<MythicStyledTooltip title={cellData?.button?.hoverText || "View Data"} >
<Button size="small" variant="contained" color="primary"
onClick={() => setOpenButton(true)} disabled={cellData?.button?.disabled || false}
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.startIconColor || ""}}/> : null}
>{cellData?.button?.name || " "}</Button>
>{cellData?.button?.name || "View Data"}</Button>
</MythicStyledTooltip>
{openButton &&
<MythicDisplayTextDialog fullWidth={true} maxWidth="lg" open={openButton} title={cellData?.button?.title || "Title Here"} value={cellData?.button?.value || ""}
@@ -287,11 +287,11 @@ const ResponseDisplayTableActionCell = ({cellData, callback_id, rowData}) => {
case "table":
return (
<React.Fragment>
<MythicStyledTooltip title={cellData?.button?.hoverText || " "} >
<MythicStyledTooltip title={cellData?.button?.hoverText || "View Data"} >
<Button size="small" variant="contained" color="primary"
onClick={() => setOpenButton(true)} disabled={cellData?.button?.disabled || false}
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.startIconColor || ""}}/> : null}
>{cellData?.button?.name || " "}</Button>
>{cellData?.button?.name || "View Data"}</Button>
</MythicStyledTooltip>
{openButton &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openButton}
@@ -305,10 +305,10 @@ const ResponseDisplayTableActionCell = ({cellData, callback_id, rowData}) => {
case "task":
return (
<React.Fragment>
<MythicStyledTooltip title={cellData?.button?.hoverText || " "}>
<MythicStyledTooltip title={cellData?.button?.hoverText || "Submit Task"}>
<Button size="small" onClick={() => setOpenTaskingButton(true)} disabled={cellData?.button?.disabled || false} variant="contained" color="warning"
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.startIconColor || ""}}/> : null}
>{cellData?.button?.name || " "}</Button>
>{cellData?.button?.name || "Submit Task"}</Button>
</MythicStyledTooltip>
{openTaskingButton &&
<TaskFromUIButton ui_feature={cellData?.button?.ui_feature || " "}
@@ -5,17 +5,20 @@ import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import { toLocalTime } from '../../utilities/Time';
import Accordion from '@mui/material/Accordion';
import AccordionDetails from '@mui/material/AccordionDetails';
import AccordionSummary from '@mui/material/AccordionSummary';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import ChatOutlinedIcon from '@mui/icons-material/ChatOutlined';
import Badge from '@mui/material/Badge';
import {useTheme} from '@mui/material/styles';
import {gql, useLazyQuery, useSubscription } from '@apollo/client';
import {gql, useSubscription } from '@apollo/client';
import {TaskDisplayContainer, TaskDisplayContainerConsole} from './TaskDisplayContainer';
import {TagsDisplay} from '../../MythicComponents/MythicTag';
import {taskingDataFragment} from './CallbackMutations';
import {useMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
import PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwoTone';
import CropRotateTwoToneIcon from '@mui/icons-material/CropRotateTwoTone';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
const PREFIX = 'TaskDisplay';
@@ -35,6 +38,8 @@ const accordionClasses = {
content: `${ACCORDION_PREFIX}-content`,
expandIcon: `${ACCORDION_PREFIX}-expandIcon`,
expanded: `${ACCORDION_PREFIX}-expanded`,
details: `${ACCORDION_PREFIX}-details`,
detailsRoot: `${ACCORDION_PREFIX}Details-root`
}
const StyledPaper = styled(Paper)((
@@ -44,11 +49,11 @@ const StyledPaper = styled(Paper)((
) => ({
[`&.${classes.root}`]: {
marginTop: "3px",
marginLeft: "3px",
marginRight: "0px",
height: "auto",
width: "99%",
boxShadow: "unset",
backgroundColor: theme.palette.background.taskLabel,
},
[`& .${classes.heading}`]: {
@@ -57,50 +62,44 @@ const StyledPaper = styled(Paper)((
cursor: "default",
wordBreak: "break-all",
},
[`& .${classes.secondaryHeading}`]: {
fontSize: theme.typography.pxToRem(15),
//color: theme.palette.text.secondary,
overflow: "auto",
display: "block",
textOverflow: "ellipsis",
overflow: "auto",
display: "block",
textOverflow: "ellipsis",
wordBreak: "break-all",
maxWidth: "100%",
maxWidth: "100%",
},
[`& .${classes.taskAndTimeDisplay}`]: {
fontSize: theme.typography.pxToRem(12),
fontSize: theme.typography.pxToRem(14),
color: theme.palette.text.secondary,
overflow: "hidden",
textOverflow: "ellipsis",
maxWidth: "100%",
overflow: "hidden",
textOverflow: "ellipsis",
maxWidth: "100%",
whiteSpace: "nowrap",
display: "inline-block",
cursor: "default",
wordBreak: "break-all",
},
[`& .${classes.secondaryHeadingExpanded}`]: {
fontSize: theme.typography.pxToRem(15),
//color: theme.palette.text.secondary,
display: "block",
display: "block",
overflow: "auto",
maxWidth: "100%",
maxWidth: "100%",
whiteSpace: "break-all",
wordBreak: "break-all",
},
[`& .${classes.icon}`]: {
verticalAlign: 'middle',
height: 20,
width: 20,
},
[`& .${classes.details}`]: {
alignItems: 'center',
marginRight: 0
},
[`& .${classes.column}`]: {
padding: "0 5px 0 0",
display: "inline-block",
@@ -109,54 +108,6 @@ const StyledPaper = styled(Paper)((
}
}));
/*
export const taskDataFragment = gql`
fragment taskData on task {
comment
callback_id
commentOperator{
username
}
completed
id
display_id
operator{
username
}
original_params
display_params
status
timestamp
command {
cmd
supported_ui_features
id
}
command_name
response_count
opsec_pre_blocked
opsec_pre_bypassed
opsec_post_blocked
opsec_post_bypassed
tasks {
id
}
tags {
tagtype {
name
color
id
}
id
}
token {
id
}
}
`;
*/
// task(where: {parent_task_id: {_eq: $task_id}}, order_by: {id: asc}) {
const getSubTaskingQuery = gql`
${taskingDataFragment}
subscription getSubTasking($task_id: Int!){
@@ -180,6 +131,7 @@ const StyledAccordionSummary = styled(AccordionSummary)((
wordBreak: "break-all",
userSelect: "text",
boxShadow: "unset",
backgroundColor: "unset",
},
[`& .${accordionClasses.content}`]: {
margin: 0,
@@ -190,10 +142,11 @@ const StyledAccordionSummary = styled(AccordionSummary)((
margin: 0,
},
[`& .${accordionClasses.expanded}`]: {
marginRight: 0
marginRight: 0,
},
}));
function TaskDisplayPreMemo({task, me, filterOptions, newlyIssuedTasks}){
return (
<TaskRow me={me} task={task} newlyIssuedTasks={newlyIssuedTasks} filterOptions={filterOptions}
@@ -219,21 +172,21 @@ function TaskDisplayConsolePreMemo({task, me, filterOptions, newlyIssuedTasks}){
export const TaskDisplayConsole = React.memo(TaskDisplayConsolePreMemo);
const TaskStatusDisplay = ({task, theme}) => {
if(task.status.toLowerCase().includes("error")){
return (<Typography size="small" component="span" style={{padding: "0", color: theme.palette.error.main, marginLeft: "5%", display: "inline-block", fontSize: theme.typography.pxToRem(15)}}>{task.status.toLowerCase()}</Typography>)
return (<Typography size="small" component="span" style={{color: theme.palette.error.main, display: "inline-block", fontSize: theme.typography.pxToRem(15)}}>{task.status.toLowerCase()}</Typography>)
}else if(task.status === "cleared"){
return (<Typography size="small" component="span" style={{padding: "0", color: theme.palette.warning.main, marginLeft: "5%", display: "inline-block", fontSize: theme.typography.pxToRem(15)}}>cleared</Typography>)
return (<Typography size="small" component="span" style={{padding: "0", color: theme.palette.warning.main, display: "inline-block", fontSize: theme.typography.pxToRem(15)}}>cleared</Typography>)
}else if(task.status === "completed" || task.status === "success"){
return null//return (<Typography size="small" style={{padding: "0", color: theme.palette.success.main, marginLeft: "5%", display: "inline-block", fontSize: theme.typography.pxToRem(15)}}>completed</Typography>)
}else if(task.status === "submitted"){
return (<Typography size="small" component="span" style={{padding: "0", color: theme.palette.info.main, marginLeft: "5%", display: "inline-block", fontSize: theme.typography.pxToRem(15)}}>{task.status.toLowerCase()}</Typography>)
return (<Typography size="small" component="span" style={{padding: "0", color: theme.palette.info.main, display: "inline-block", fontSize: theme.typography.pxToRem(15)}}>{task.status.toLowerCase()}</Typography>)
}else if(task.status.toLowerCase().includes("processing")){
return (<Typography size="small" component="span" style={{padding: "0", color: theme.palette.warning.main, marginLeft: "5%", display: "inline-block", fontSize: theme.typography.pxToRem(15)}}>{task.status.toLowerCase()}</Typography>)
return (<Typography size="small" component="span" style={{padding: "0", color: theme.palette.warning.main, display: "inline-block", fontSize: theme.typography.pxToRem(15)}}>{task.status.toLowerCase()}</Typography>)
}else if(task.opsec_pre_blocked && !task.opsec_pre_bypassed){
return (<Typography size="small" component="span" style={{padding: "0", color: theme.palette.warning.main, marginLeft: "5%", display: "inline-block", fontSize: theme.typography.pxToRem(15)}}>OPSEC BLOCKED (PRE)</Typography>)
return (<Typography size="small" component="span" style={{padding: "0", color: theme.palette.warning.main, display: "inline-block", fontSize: theme.typography.pxToRem(15)}}>OPSEC BLOCKED (PRE)</Typography>)
}else if(task.opsec_post_blocked && !task.opsec_post_bypassed){
return (<Typography size="small" component="span" style={{padding: "0", color: theme.palette.warning.main, marginLeft: "5%", display: "inline-block", fontSize: theme.typography.pxToRem(15)}}>OPSEC BLOCKED (POST)</Typography>)
return (<Typography size="small" component="span" style={{padding: "0", color: theme.palette.warning.main, display: "inline-block", fontSize: theme.typography.pxToRem(15)}}>OPSEC BLOCKED (POST)</Typography>)
}else{
return (<Typography size="small" component="span" style={{padding: "0", color: theme.palette.info.main, marginLeft: "5%", display: "inline-block", fontSize: theme.typography.pxToRem(15)}}>{task.status.toLowerCase()}</Typography>)
return (<Typography size="small" component="span" style={{padding: "0", color: theme.palette.info.main, display: "inline-block", fontSize: theme.typography.pxToRem(15)}}>{task.status.toLowerCase()}</Typography>)
}
}
const TaskTagDisplay = ({task}) => {
@@ -241,7 +194,7 @@ const TaskTagDisplay = ({task}) => {
<TagsDisplay tags={task.tags} />
)
}
const ColoredTaskDisplay = ({task, theme, children}) => {
const ColoredTaskDisplay = ({task, theme, children, expanded}) => {
const [themeColor, setThemeColor] = React.useState(theme.palette.info.main);
useEffect( () => {
if(task.status.toLowerCase().includes("error")){
@@ -263,12 +216,34 @@ const ColoredTaskDisplay = ({task, theme, children}) => {
}
}, [task.status, task.completed])
return(
<span style={{display: "flex", margin: 0, borderWidth: 0, padding: 0, minHeight: "48px", alignItems: "center", height: "100%", borderLeft: "6px solid " + themeColor, paddingLeft: "5px", width: "100%"}}>
<span style={{display: "flex", margin: 0, borderWidth: 0, padding: 0, minHeight: "48px", alignItems: "center", height: "100%", borderLeft: "6px solid " + themeColor, paddingLeft: "5px", width: "100%", borderTopLeftRadius: "4px", borderBottomLeftRadius: expanded ? 0 : "4px"}}>
{children}
</span>
)
}
const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick }) => {
const GetOperatorDisplay = ({initialHideUsernameValue, task}) => {
if(initialHideUsernameValue){
return '';
}
if(task?.eventstepinstance !== null){
return (
<span style={{display: "inline-flex", alignItems: "center"}}>
{"/ "}
<IconButton component={Link} href={'/new/eventing?eventgroup=' +
task?.eventstepinstance?.eventgroupinstance?.eventgroup?.id +
"&eventgroupinstance=" + task?.eventstepinstance?.eventgroupinstance?.id
} target={"_blank"} style={{padding: 0}}
color="inherit" disableFocusRipple={true}
disableRipple={true}>
<PlayCircleFilledTwoToneIcon />
</IconButton>
{task.operator.username}
</span>
)
}
return "/ " + task.operator.username;
}
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"});
@@ -290,8 +265,9 @@ const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick }) => {
}
preventPropagation(e);
}
return (
<ColoredTaskDisplay task={task} theme={theme} >
<ColoredTaskDisplay task={task} theme={theme} expanded={expanded} >
<div id={taskDivID} style={{width: "100%"}}>
{displayComment ? (
<React.Fragment>
@@ -302,23 +278,47 @@ const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick }) => {
<div >
<Typography className={classes.taskAndTimeDisplay} onClick={preventPropagation}>
[{toLocalTime(task.timestamp, me?.user?.view_utc_time || false)}]
/ {task.display_id} {initialHideUsernameValue ? '' : `/ ${task.operator.username} `}
/ <Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank" href={"/new/callbacks/" + task.callback.display_id}>{ task.callback.display_id}</Link>
{" / "}
<span style={{display: "inline-flex", alignItems: "center"}}>
{task.has_intercepted_response &&
<MythicStyledTooltip
title={"This task has responses that have been intercepted and changed due to a workflow container"}>
<IconButton style={{padding: 0}} color={"secondary"}>
<CropRotateTwoToneIcon fontSize={"small"}/>
</IconButton>
</MythicStyledTooltip>
}
<Link style={{wordBreak: "break-all"}} color={"textPrimary"} underline={"always"} target={"_blank"}
href={"/new/task/" + task.display_id}>{task.display_id}</Link>
</span>
{" "}
<GetOperatorDisplay initialHideUsernameValue={initialHideUsernameValue} task={task}/>
{" / "} <Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
href={"/new/callbacks/" + task.callback.display_id}>{task.callback.display_id}</Link>
{initialShowHostnameValue ? ` / ${task.callback.host} ` : ''}
{initialShowIPValue ? `/ ${ipValue} ` : ''}
{initialShowCallbackGroupsValue ? `/ ${task.callback.mythictree_groups.join(', ')} ` : ''}
{" / "}
<TaskStatusDisplay task={task} theme={theme}/>
</Typography>
<TaskStatusDisplay task={task} theme={theme}/>
<TaskTagDisplay task={task} />
<TaskTagDisplay task={task}/>
</div>
<div>
{task.comment !== "" ? (
<div className={classes.column}>
<IconButton size="small" style={{padding: "0"}} color="primary" onClick={toggleDisplayComment}><ChatOutlinedIcon/></IconButton>
<IconButton size="small" style={{padding: "0"}} color="primary"
onClick={toggleDisplayComment}><ChatOutlinedIcon/></IconButton>
</div>
) : null}
<div className={classes.column} onClick={onLocalClick}>
<Badge badgeContent={alertBadges} color="warning" anchorOrigin={{vertical: 'top', horizontal: 'left'}}>
{task.tasks.length > 0 && !displayChildren &&
<ExpandMoreIcon onClick={toggleDisplayChildren} />
}
{task.tasks.length > 0 && displayChildren &&
<ExpandLessIcon onClick={toggleDisplayChildren} />
}
<Typography className={classes.heading} onClick={onLocalClick}>
{(task?.command?.cmd || task.command_name) + " " + task.display_params}
</Typography>
@@ -333,6 +333,7 @@ const TaskRow = ({task, filterOptions, me, newlyIssuedTasks, indentLevel}) => {
const [dropdownOpen, setDropdownOpen] = React.useState(false);
const [taskingData, setTaskingData] = React.useState([]);
const [shouldDisplay, setShouldDisplay] = React.useState(true);
const [displayChildren, setDisplayChildren] = React.useState(false);
useSubscription(getSubTaskingQuery, {
variables: {task_id: task.id},
onData: ({data}) => {
@@ -413,7 +414,7 @@ const TaskRow = ({task, filterOptions, me, newlyIssuedTasks, indentLevel}) => {
}
}
if(filterOptions["hideErrors"]){
if(task.status.includes("error")){
if(task.status.toLowerCase().includes("error")){
if(shouldDisplay){
setShouldDisplay(false);
}
@@ -423,13 +424,19 @@ const TaskRow = ({task, filterOptions, me, newlyIssuedTasks, indentLevel}) => {
if(!shouldDisplay){
setShouldDisplay(true);
}
}, [filterOptions, task.comment, task.command, task.display_params, task.operator.username]);
}, [filterOptions, task.comment, task.command, task.status, task.display_params, task.operator.username]);
const toggleTaskDropdown = React.useCallback( (event, expanded) => {
if(window.getSelection().toString() !== ""){
return;
}
setDropdownOpen(!dropdownOpen);
}, [dropdownOpen]);
const toggleDisplayChildren = React.useCallback( (event, expanded) => {
if(window.getSelection().toString() !== ""){
return;
}
setDisplayChildren(!displayChildren);
}, [displayChildren]);
/*
useEffect( () => {
if(!isFetchingSubtasks && task.tasks.length > 0){
@@ -441,8 +448,9 @@ const TaskRow = ({task, filterOptions, me, newlyIssuedTasks, indentLevel}) => {
shouldDisplay ? (
<div style={{marginLeft: (indentLevel * 10) + "px"}}>
<TaskLabel me={me} task={task} newlyIssuedTasks={newlyIssuedTasks} dropdownOpen={dropdownOpen}
toggleTaskDropdown={toggleTaskDropdown}/>
{
toggleTaskDropdown={toggleTaskDropdown}
toggleDisplayChildren={toggleDisplayChildren} displayChildren={displayChildren}/>
{ displayChildren &&
taskingData.map( (tsk) => (
<TaskRow key={"taskrow: " + tsk.id} me={me} task={tsk}
filterOptions={filterOptions} indentLevel={indentLevel+1}/>
@@ -456,6 +464,7 @@ const TaskRow = ({task, filterOptions, me, newlyIssuedTasks, indentLevel}) => {
const TaskRowFlat = ({task, filterOptions, me, onSelectTask, showOnSelectTask, selectedTask, indentLevel}) => {
const [taskingData, setTaskingData] = React.useState([]);
const [shouldDisplay, setShouldDisplay] = React.useState(true);
const [displayChildren, setDisplayChildren] = React.useState(false);
useSubscription(getSubTaskingQuery, {
variables: {task_id: task.id},
onData: ({data}) => {
@@ -563,15 +572,23 @@ const TaskRowFlat = ({task, filterOptions, me, onSelectTask, showOnSelectTask, s
}
setTaskingData(updated)
}, [selectedTask]);
const toggleDisplayChildren = React.useCallback( (event, expanded) => {
if(window.getSelection().toString() !== ""){
return;
}
setDisplayChildren(!displayChildren);
event.stopPropagation();
event.preventDefault();
}, [displayChildren]);
return (
shouldDisplay && (
<div style={{marginLeft: (indentLevel * 10) + "px"}}>
<TaskLabelFlat me={me} task={task}
onSelectTask={() => {onSelectTask(task)}}
showOnSelectTask={showOnSelectTask}
toggleDisplayChildren={toggleDisplayChildren} displayChildren={displayChildren}
/>
{
{ displayChildren &&
taskingData.map( (tsk) => (
<TaskRowFlat key={"taskrow: " + tsk.id} indentLevel={indentLevel+1}
me={me} task={tsk} onSelectTask={()=>{onSelectTask(tsk)}}
@@ -708,14 +725,14 @@ const TaskRowConsole = ({task, filterOptions, me, newlyIssuedTasks, indentLevel}
)
}
const TaskLabel = ({task, dropdownOpen, toggleTaskDropdown, me, newlyIssuedTasks}) => {
const TaskLabel = ({task, dropdownOpen, toggleTaskDropdown, me, newlyIssuedTasks, displayChildren, toggleDisplayChildren}) => {
const [fromNow, setFromNow] = React.useState(new Date());
const theme = useTheme();
const prevResponseMaxId = useRef(0);
// only scroll down for your own tasks
useLayoutEffect( () => {
if(task.operator.username === (me?.user?.username || "")){
scrollContent();
scrollContent(); // need to change this if we want to prevent auto scroll on expanding subtasks
}
}, [])
useEffect( () => {
@@ -753,19 +770,23 @@ const TaskLabel = ({task, dropdownOpen, toggleTaskDropdown, me, newlyIssuedTasks
}
return (
<StyledPaper className={classes.root + " no-box-shadow"} elevation={5} style={{marginRight: 0}} id={`taskHeader-${task.id}`}>
<Accordion TransitionProps={{ unmountOnExit: true, onEntered: scrollContent }} defaultExpanded={false} onChange={toggleTaskDropdown} expanded={dropdownOpen} >
<StyledPaper className={classes.root + " no-box-shadow"} elevation={5} id={`taskHeader-${task.id}`}>
<Accordion TransitionProps={{ unmountOnExit: true, onEntered: scrollContent }} defaultExpanded={false}
onChange={toggleTaskDropdown} expanded={dropdownOpen}
style={{backgroundColor: "unset", backgroundImage: "unset"}}
>
<StyledAccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={`panel1c-content-task-${task.id}`}
id={`panel1c-header-${task.id}`}
classes={accordionClasses}
>
<ColoredTaskLabel theme={theme} task={task} me={me} taskDivID={'scrolltotask' + task.id} />
>
<ColoredTaskLabel theme={theme} task={task} me={me} taskDivID={'scrolltotask' + task.id}
displayChildren={displayChildren} toggleDisplayChildren={toggleDisplayChildren}
expanded={dropdownOpen}
/>
</StyledAccordionSummary>
<AccordionDetails style={{cursor: "default"}}>
<TaskDisplayContainer me={me} task={task} />
</AccordionDetails>
<TaskDisplayContainer me={me} task={task} />
</Accordion>
</StyledPaper>
);
@@ -779,7 +800,7 @@ export const getLabelText = (task, graphView) => {
}
return (task?.command?.cmd || task.command_name) + " " + task.display_params;
}
export const TaskLabelFlat = ({task, me, showOnSelectTask, onSelectTask, graphView}) => {
export const TaskLabelFlat = ({task, me, showOnSelectTask, onSelectTask, graphView, displayChildren, toggleDisplayChildren}) => {
const theme = useTheme();
useLayoutEffect( () => {
@@ -811,7 +832,9 @@ export const TaskLabelFlat = ({task, me, showOnSelectTask, onSelectTask, graphVi
elevation={5} style={{marginRight: 0, cursor: "pointer"}} id={`taskHeader-${task.id}`}
onClick={onClickEntry}
>
<ColoredTaskLabel theme={theme} task={task} me={me} taskDivID={`scrolltotasksplit${task.id}`} onClick={onClickEntry} />
<ColoredTaskLabel theme={theme} task={task} me={me} taskDivID={`scrolltotasksplit${task.id}`} onClick={onClickEntry}
displayChildren={displayChildren} toggleDisplayChildren={toggleDisplayChildren} expanded={false}
/>
</StyledPaper>
)
}
@@ -836,7 +859,8 @@ const TaskLabelConsole = ({task, me}) => {
return (
<StyledPaper className={classes.root + " no-box-shadow"} elevation={5} style={{marginRight: 0}} id={`taskHeader-${task.id}`}>
<ColoredTaskLabel theme={theme} task={task} me={me} taskDivID={`scrolltotaskconsole${task.id}`} />
<ColoredTaskLabel theme={theme} task={task} me={me} taskDivID={`scrolltotaskconsole${task.id}`}
expanded={true}/>
<TaskDisplayContainerConsole me={me} task={task} />
</StyledPaper>
);
@@ -27,7 +27,6 @@ import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons';
import { faExternalLinkAlt, faExpandArrowsAlt } from '@fortawesome/free-solid-svg-icons';
import SearchIcon from '@mui/icons-material/Search';
import SpeedDial from '@mui/material/SpeedDial';
import SpeedDialIcon from '@mui/material/SpeedDialIcon';
import SpeedDialAction from '@mui/material/SpeedDialAction';
import { Backdrop } from '@mui/material';
import {downloadFileFromMemory} from '../../utilities/Clipboard';
@@ -38,6 +38,9 @@ const getLoadedCommandsBasedOnInput = ({cmd, ui_feature}) => {
id
needs_admin
payload_type_id
payloadtype {
name
}
attributes
commandparameters {
id
@@ -128,7 +131,7 @@ export const TaskFromUIButton = ({callback_id, callback_ids, cmd, ui_feature, pa
onTasked({tasked: false});
return;
}
const availableCommands = data.callback_by_pk.loadedcommands.reduce( (prev, cur) => {
let availableCommands = data.callback_by_pk.loadedcommands.reduce( (prev, cur) => {
if(typeof(parameters) === "string"){
return [...prev, {...cur.command, "parsedParameters": {}}];
}else{
@@ -141,6 +144,7 @@ export const TaskFromUIButton = ({callback_id, callback_ids, cmd, ui_feature, pa
return [...prev, {...cur.token, "display": renderValue(cur.token)}]
}, []);
setCallbackTokenOptions(availableTokens);
availableCommands = availableCommands.map(c => {return {...c, display: `${c.cmd} ( ${c.payloadtype.name} )`}});
setFileBrowserCommands(availableCommands);
if(availableCommands.length === 0){
if(ui_feature !== undefined){
@@ -226,7 +230,7 @@ export const TaskFromUIButton = ({callback_id, callback_ids, cmd, ui_feature, pa
createTask({variables: {...variables, callback_id: callbackData.callback_by_pk.display_id}})
}
}
const submitParametersDialog = (cmd, new_parameters, files) => {
const submitParametersDialog = (cmd, new_parameters, files, selectedParameterGroup, payload_type) => {
setOpenParametersDialog(false);
try{
savedFinalVariables.current = JSON.parse(new_parameters);
@@ -238,7 +242,14 @@ export const TaskFromUIButton = ({callback_id, callback_ids, cmd, ui_feature, pa
savedFinalVariables.current = new_parameters;
}
onSubmitTasking({variables: {callback_id: callbackData.callback_by_pk.display_id, command: cmd, params: new_parameters, files, tasking_location: "modal"}});
onSubmitTasking({variables: {callback_id: callbackData.callback_by_pk.display_id,
command: cmd,
params: new_parameters,
files,
tasking_location: "modal",
payload_type: payload_type,
parameter_group_name: selectedParameterGroup
}});
}
const onSubmitSelectedToken = (token) => {
setSelectedCallbackToken(token);
@@ -332,7 +343,7 @@ export const TaskFromUIButton = ({callback_id, callback_ids, cmd, ui_feature, pa
onClose={()=>{setOpenSelectCommandDialog(false);onTasked({tasked: false});}}
innerDialog={<MythicSelectFromListDialog onClose={()=>{setOpenSelectCommandDialog(false);onTasked({tasked: false});}}
onSubmit={onSubmitSelectedCommand} options={fileBrowserCommands} title={"Select Command"}
action={"select"} identifier={"id"} display={"cmd"} dontCloseOnSubmit={true} />}
action={"select"} identifier={"id"} display={"display"} dontCloseOnSubmit={true} />}
/>
}
{openSelectCallback &&
@@ -10,7 +10,6 @@ import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import {TaskParametersDialogRow} from './TaskParametersDialogRow';
import {gql, useLazyQuery, useMutation, useQuery} from '@apollo/client';
import FormControl from '@mui/material/FormControl';
@@ -20,7 +19,6 @@ import Input from '@mui/material/Input';
import {UploadTaskFile} from '../../MythicComponents/MythicFileUpload';
import {Backdrop, CircularProgress} from '@mui/material';
import Divider from '@mui/material/Divider';
import {useTheme} from '@mui/material/styles';
import {b64DecodeUnicode} from './ResponseDisplay';
import {snackActions} from "../../utilities/Snackbar";
@@ -287,7 +285,6 @@ export const commandInParsedParameters = (cmd, parsedParameters) =>{
return undefined
}
export function TaskParametersDialog(props) {
const theme = useTheme();
const [backdropOpen, setBackdropOpen] = React.useState(false);
const [commandInfo, setCommandInfo] = useState({});
const [parameterGroups, setParameterGroups] = useState([]);
@@ -484,11 +481,17 @@ export function TaskParametersDialog(props) {
if(parsedParameterName){
return [...prev, {...cmd, value: props.command.parsedParameters[parsedParameterName]}];
}else if(cmd.default_value.length > 0){
return [...prev, {...cmd, value: JSON.parse(cmd.default_value)}];
try {
return [...prev, {...cmd, value: JSON.parse(cmd.default_value)}];
}catch(error){
return [...prev, {...cmd, value: [[cmd.default_value, ""]] }];
}
}else{
return [...prev, {...cmd, value: []}];
}
case "ChooseOne":
case "ChooseOneCustom":
case "ChooseMultiple":
let choices = cmd.choices;
let defaultV = cmd.default_value;
@@ -558,7 +561,9 @@ export function TaskParametersDialog(props) {
return [...prev, {...cmd, choices: choices, default_value: defaultV, value: defaultV}];
}
case "File":
return [...prev, {...cmd, value: {} }];
return [...prev, {...cmd, value: {} }];
case "FileMultiple":
return [...prev, {...cmd, value: []}];
case "CredentialJson":
if (loadedCredentialsLoading.credential.length > 0){
if(parsedParameterName){
@@ -816,6 +821,7 @@ export function TaskParametersDialog(props) {
case "Boolean":
case "Number":
case "ChooseOne":
case "ChooseOneCustom":
case "ChooseMultiple":
case "PayloadList":
case "Array":
@@ -832,16 +838,37 @@ export function TaskParametersDialog(props) {
collapsedParameters[param.name] = param.value;
break
case "File":
setBackdropOpen(true);
const newUUID = await UploadTaskFile(param.value, "Uploaded as part of tasking");
if(newUUID){
newFileUUIDs.push(newUUID);
collapsedParameters[param.name] = newUUID;
if(newUUID !== "Missing file in form"){
newFileUUIDs.push(newUUID);
collapsedParameters[param.name] = newUUID;
}
}else{
setBackdropOpen(false);
return;
}
break;
case "FileMultiple":
let fileIDs = [];
for(let i = 0; i < param.value.length; i++){
if(typeof param.value[i] === "string"){
fileIDs.push(param.value[i]);
continue
}
const newUUID = await UploadTaskFile(param.value[i], "Uploaded as part of tasking");
if(newUUID){
if(newUUID !== "Missing file in form"){
newFileUUIDs.push(newUUID);
fileIDs.push(newUUID);
} else {
snackActions.warning("Failed to upload file");
}
} else {
snackActions.warning("Failed to upload file");
}
}
collapsedParameters[param.name] = fileIDs;
break;
case "CredentialJson":
collapsedParameters[param.name] = {
account: param.value["account"],
@@ -856,7 +883,7 @@ export function TaskParametersDialog(props) {
}
}
setBackdropOpen(false);
props.onSubmit(props.command.cmd, JSON.stringify(collapsedParameters), newFileUUIDs, selectedParameterGroup);
props.onSubmit(props.command.cmd, JSON.stringify(collapsedParameters), newFileUUIDs, selectedParameterGroup, props.command?.payload_type?.name);
}
const onAgentConnectAddNewPayloadOnHost = (host, payload) => {
@@ -9,7 +9,6 @@ import Switch from '@mui/material/Switch';
import Input from '@mui/material/Input';
import {Button, IconButton} from '@mui/material';
import MythicTextField from '../../MythicComponents/MythicTextField';
import Paper from '@mui/material/Paper';
import TableHead from '@mui/material/TableHead';
import AddCircleIcon from '@mui/icons-material/AddCircle';
import DeleteIcon from '@mui/icons-material/Delete';
@@ -24,6 +23,7 @@ import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip'
import { Backdrop } from '@mui/material';
import {CircularProgress} from '@mui/material';
import MythicStyledTableCell from '../../MythicComponents/MythicTableCell';
import {MythicFileContext} from "../../MythicComponents/MythicFileContext";
const getDynamicQueryParams = gql`
mutation getDynamicParamsMutation($callback: Int!, $command: String!, $payload_type: String!, $parameter_name: String!){
@@ -77,6 +77,7 @@ export function TaskParametersDialogRow(props){
const [arrayValue, setArrayValue] = React.useState([""]);
const [typedArrayValue, setTypedArrayValue] = React.useState([]);
const [choiceMultipleValue, setChoiceMultipleValue] = React.useState([]);
const [chooseOneCustomValue, setChooseOneCustomValue] = React.useState("");
const [agentConnectNewHost, setAgentConnectNewHost] = React.useState("");
const [agentConnectHostOptions, setAgentConnectHostOptions] = React.useState([]);
const [agentConnectNewPayload, setAgentConnectNewPayload] = React.useState(0);
@@ -88,6 +89,7 @@ export function TaskParametersDialogRow(props){
const [openAdditionalPayloadOnHostMenu, setOpenAdditionalPayloadOnHostmenu] = React.useState(false);
const [createCredentialDialogOpen, setCreateCredentialDialogOpen] = React.useState(false);
const [fileValue, setFileValue] = React.useState({name: ""});
const [fileMultValue, setFileMultValue] = React.useState([]);
const [backdropOpen, setBackdropOpen] = React.useState(false);
const usingDynamicParamChoices = React.useRef(false);
const usingParsedTypedArray = React.useRef(true);
@@ -192,6 +194,8 @@ export function TaskParametersDialogRow(props){
}
}else if(props.type === "Array") {
setArrayValue(props.value);
}else if(props.type === "FileMultiple"){
setFileMultValue(props.value);
}else if(props.type === "TypedArray"){
if(value === ""){
//console.log(props.value);
@@ -352,6 +356,15 @@ export function TaskParametersDialogRow(props){
setValue(value);
props.onChange(props.name, value, error);
}
const onChangeTextChooseOneCustom = (name, newValue, error) => {
setChooseOneCustomValue(newValue);
if(newValue === ""){
props.onChange(props.name, value, error);
} else {
props.onChange(props.name, newValue, error);
}
}
const onChangeNumber = (name, value, error) => {
setValue(parseInt(value));
props.onChange(props.name, parseInt(value), error);
@@ -364,7 +377,10 @@ export function TaskParametersDialogRow(props){
const onFileChange = (evt) => {
setFileValue({name: evt.target.files[0].name});
props.onChange(props.name, evt.target.files[0]);
}
const onFileMultChange = (evt) => {
setFileMultValue([...evt.target.files]);
props.onChange(props.name, [...evt.target.files]);
}
const onChangeAgentConnectHost = (event) => {
setAgentConnectHost(event.target.value);
@@ -498,6 +514,39 @@ export function TaskParametersDialogRow(props){
}
const getParameterObject = () => {
switch(props.type){
case "ChooseOneCustom":
return (
<React.Fragment>
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={false}>
<CircularProgress color="inherit" />
</Backdrop>
<div style={{width: "100%", display: "flex", alignItems: "center"}}>
<FormControl style={{}}>
<Select
native
autoFocus={props.autoFocus}
multiple={false}
value={value}
disabled={chooseOneCustomValue !== ""}
onChange={onChangeValue}
input={<Input />}
>
{
ChoiceOptions.map((opt, i) => (
<option key={props.name + i} value={opt}>{opt}</option>
))
}
</Select>
</FormControl>
OR
<MythicTextField required={props.required} placeholder={"Custom Value"} value={chooseOneCustomValue} multiline={true} maxRows={5}
onChange={onChangeTextChooseOneCustom} display="inline-block" onEnter={props.onSubmit} autoFocus={props.autoFocus}
name={props.name}
/>
</div>
</React.Fragment>
)
case "ChooseOne":
case "ChooseMultiple":
return (
@@ -620,9 +669,30 @@ 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>
<>
<Button variant="contained" component="label">
{ fileValue.name === "" ? "Select File" : fileValue.name }
<input onChange={onFileChange} type="file" hidden />
</Button>
</>
)
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>
))
}
</>
)
case "LinkInfo":
return (
@@ -663,7 +733,7 @@ export function TaskParametersDialogRow(props){
)
case "AgentConnect":
return (
<TableContainer component={Paper} className="mythicElement">
<>
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "100%", "overflow": "auto"}}>
<TableBody>
{openAdditionalPayloadOnHostMenu ? (
@@ -799,7 +869,7 @@ export function TaskParametersDialogRow(props){
</TableBody>
</Table>
): null}
</TableContainer>
</>
)
case "CredentialJson":
return (
@@ -90,10 +90,7 @@ export function TaskTokenDialog(props) {
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Token Information</DialogTitle>
<Paper elevation={5} style={{position: "relative"}} variant={"elevation"}>
<TableContainer component={Paper} className="mythicElement">
<TableContainer className="mythicElement">
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
<TableHead>
<TableRow>
@@ -111,7 +108,6 @@ export function TaskTokenDialog(props) {
</TableBody>
</Table>
</TableContainer>
</Paper>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
Close
@@ -25,6 +25,10 @@ query getParametersQuery ($task_id: Int!) {
command {
cmd
id
payloadtype {
name
id
}
}
}
}
@@ -46,6 +50,7 @@ export function TaskViewParametersDialog(props) {
workingComment += "\n\nOriginal Command: " + data.task_by_pk.command.cmd;
workingComment += "\nIssued Command: " + data.task_by_pk.command_name;
}
workingComment += "\n\nPayload Type:\n" + data.task_by_pk.command.payloadtype.name;
}
setComment(workingComment);
},
@@ -1,6 +1,7 @@
import React from 'react';
import TableRow from '@mui/material/TableRow';
import Table from '@mui/material/Table';
import TableCell from '@mui/material/TableCell';
import TableBody from '@mui/material/TableBody';
import TableHead from '@mui/material/TableHead';
import {useQuery, gql} from '@apollo/client';
@@ -21,11 +22,12 @@ import LinearProgress from '@mui/material/LinearProgress';
import { Backdrop } from '@mui/material';
import {CircularProgress} from '@mui/material';
import {snackActions} from "../../utilities/Snackbar";
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
const getCallbackMythicTreeGroups = gql`
query getCallbackMythicTreeGroups($group_name: [String!]!) {
callback(where: {mythictree_groups: {_contains: $group_name}}) {
callback(where: {mythictree_groups: {_contains: $group_name}}, order_by: {id: asc}) {
display_id
user
host
@@ -44,7 +46,7 @@ query getCallbackMythicTreeGroups($group_name: [String!]!) {
`;
const getAllCallbackMythicTreeGroups = gql`
query getCallbackMythicTreeGroups {
callback {
callback(order_by: {id: asc}) {
display_id
user
host
@@ -105,21 +107,21 @@ export function ViewCallbackMythicTreeGroupsDialog(props){
<div style={{paddingLeft: "20px"}}>
All of these callbacks are contributing data that's aggregated together for the "{props.group_name}" group.
</div>
<DialogContent dividers={true} style={{paddingLeft: 0, paddingRight: 0}}>
<DialogContent dividers={true} style={{padding: 0}}>
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={false}>
<CircularProgress color="inherit" />
</Backdrop>
<Table size="small" aria-label="details" style={{ "overflowWrap": "break-word", width: "100%"}}>
<Table stickyHeader={true} size="small" style={{ "overflowWrap": "break-word", width: "100%"}}>
<TableHead>
<TableRow>
<MythicStyledTableCell></MythicStyledTableCell>
<MythicStyledTableCell>Callback</MythicStyledTableCell>
<MythicStyledTableCell>User</MythicStyledTableCell>
<MythicStyledTableCell>Host</MythicStyledTableCell>
<MythicStyledTableCell>Domain</MythicStyledTableCell>
<MythicStyledTableCell>IP</MythicStyledTableCell>
<MythicStyledTableCell>PID</MythicStyledTableCell>
<MythicStyledTableCell>Description</MythicStyledTableCell>
<TableCell></TableCell>
<TableCell>Callback</TableCell>
<TableCell>User</TableCell>
<TableCell>Host</TableCell>
<TableCell>Domain</TableCell>
<TableCell>IP</TableCell>
<TableCell>PID</TableCell>
<TableCell>Description</TableCell>
</TableRow>
</TableHead>
@@ -137,10 +139,7 @@ export function ViewCallbackMythicTreeGroupsDialog(props){
</MythicStyledTooltip>
}
<MythicStyledTooltip title={a.payload.payloadtype.name}>
<img
style={{width: "35px", height: "35px"}}
src={"/static/" + a.payload.payloadtype.name + ".svg"}
/>
<MythicAgentSVGIcon payload_type={a.payload.payloadtype.name} style={{width: "35px", height: "35px"}} />
</MythicStyledTooltip>
</MythicStyledTableCell>
@@ -269,10 +268,7 @@ export function ViewAllCallbackMythicTreeGroupsDialog(props){
</MythicStyledTooltip>
}
<MythicStyledTooltip title={a.payload.payloadtype.name}>
<img
style={{width: "35px", height: "35px"}}
src={"/static/" + a.payload.payloadtype.name + ".svg"}
/>
<MythicAgentSVGIcon payload_type={a.payload.payloadtype.name} style={{width: "35px", height: "35px"}} />
</MythicStyledTooltip>
</MythicStyledTableCell>
@@ -1,12 +1,43 @@
import React from 'react';
import { ConsumingServicesTable } from './ConsumingServicesTable';
import {useSubscription, gql } from '@apollo/client';
const sub_consuming_services = gql`
subscription ConsumingContainer{
consuming_container_stream(batch_size: 10, cursor: {initial_value: {updated_at: "1970-01-01"}}) {
id
name
description
type
container_running
deleted
subscriptions
}
}
`;
export function ConsumingServices({me}){
const [services, setServices] = React.useState([]);
useSubscription(sub_consuming_services, {
fetchPolicy: "no-cache",
onData: ({data}) => {
const updated = data.data.consuming_container_stream.reduce( (prev, cur) => {
let existingIndex = prev.findIndex( (element, i, array) => element.id === cur.id);
if(existingIndex > -1){
prev[existingIndex] = {...prev[existingIndex], ...cur};
return [...prev];
}
return [...prev, cur];
}, services);
setServices(updated);
},
onError: ({data}) => {
console.log(data)
},
});
return (
<div style={{display: "flex", flexGrow: 1, flexDirection: "column"}}>
<div style={{display: "flex", flexDirection: "column", height: "100%"}}>
<ConsumingServicesTable
me={me}
services={services}
/>
</div>
);
@@ -0,0 +1,79 @@
import React, {useState} from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import {useQuery, gql} from '@apollo/client';
import LinearProgress from '@mui/material/LinearProgress';
import { snackActions } from '../../utilities/Snackbar';
import AceEditor from 'react-ace';
import 'ace-builds/src-noconflict/mode-json';
import 'ace-builds/src-noconflict/theme-monokai';
import 'ace-builds/src-noconflict/theme-xcode';
import "ace-builds/src-noconflict/ext-searchbox";
import {useTheme} from '@mui/material/styles';
export function ConsumingServicesGetIDPMetadataDialog(props) {
const [message, setMessage] = useState("");
const theme = useTheme();
React.useEffect( () => {
const requestOptions = {
method: "GET",
headers: {'Content-Type': 'application/json', MythicSource: "web"},
};
fetch(`/auth_metadata/${props.container}/${props.idp}`, requestOptions).then((response) => {
if(response.status !== 200){
snackActions.warning("HTTP " + response.status + " Error: Check Mythic logs");
return;
}
response.json().then(data => {
if(data["status"] === "success"){
setMessage(data["metadata"]);
} else {
setMessage("Failed to get metadata: \n" + data["error"]);
}
}).catch(error => {
snackActions.warning("Error getting JSON from server: " + error.toString());
console.log("Error trying to get json response", error, response);
});
}).catch(error => {
if(error.toString() === "TypeError: Failed to fetch"){
snackActions.warning("Please refresh and accept the SSL connection error");
} else {
snackActions.warning("Error talking to server: " + error.toString());
}
console.log("There was an error!", error);
});
}, []);
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">{props.container}'s {props.idp} Metadata</DialogTitle>
<DialogContent dividers={true}>
<AceEditor
mode="text"
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
fontSize={14}
showGutter={true}
height={"100px"}
highlightActiveLine={true}
value={message}
width={"100%"}
minLines={2}
maxLines={50}
setOptions={{
showLineNumbers: true,
tabSize: 4,
useWorker: false
}}/>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={props.onClose} color="primary">
Close
</Button>
</DialogActions>
</React.Fragment>
);
}
@@ -9,10 +9,20 @@ import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import {useTheme} from '@mui/material/styles';
import PublicIcon from '@mui/icons-material/Public';
import { IconButton } from '@mui/material';
import { useMutation, gql } from '@apollo/client';
import { snackActions } from '../../utilities/Snackbar';
import {IconButton} from '@mui/material';
import {gql, useMutation} from '@apollo/client';
import {snackActions} from '../../utilities/Snackbar';
import SyncAltIcon from '@mui/icons-material/SyncAlt';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import MythicTableCell from "../../MythicComponents/MythicTableCell";
import DeleteIcon from '@mui/icons-material/Delete';
import RestoreFromTrashOutlinedIcon from '@mui/icons-material/RestoreFromTrashOutlined';
import {MythicConfirmDialog} from "../../MythicComponents/MythicConfirmDialog";
import PermIdentityTwoToneIcon from '@mui/icons-material/PermIdentityTwoTone';
import {MythicDialog} from "../../MythicComponents/MythicDialog";
import AttachFileIcon from '@mui/icons-material/AttachFile';
import {ConsumingServicesGetIDPMetadataDialog} from "./ConsumingServicesGetIDPMetadataDialog";
import {C2ProfileListFilesDialog} from "../PayloadTypesC2Profiles/C2ProfileListFilesDialog";
const testWebhookMutation = gql`
mutation testWebhookWorks($service_type: String!){
@@ -30,35 +40,44 @@ mutation testWebhookWorks($service_type: String!){
}
}
`;
const toggleDeleteStatus = gql`
mutation toggleConsumingContainerDeleteStatus($id: Int!, $deleted: Boolean!){
update_consuming_container_by_pk(pk_columns: {id: $id}, _set: {deleted: $deleted}) {
id
}
}
`;
const webhook_events = ["new_alert","new_callback","new_custom","new_feedback", "new_startup"];
const logging_events = ["new_artifact","new_callback", "new_credential","new_file", "new_keylog", "new_payload", "new_response", "new_task"];
export function ConsumingServicesTable({servicesList}){
export function ConsumingServicesTable({services}) {
const theme = useTheme();
const [testWebhook] = useMutation(testWebhookMutation, {
onCompleted: data => {
if(data.consumingServicesTestWebhook.status === "success"){
if (data.consumingServicesTestWebhook.status === "success") {
snackActions.success("Successfully sent test message to service");
} else {
console.log(data.consumingServicesTestWebhook.error)
snackActions.error("No webhook listening")
}
},
onError: error => {
}
});
const issueTestWebook = (service_type) => {
const issueTestWebhook = (service_type) => {
testWebhook({variables: {service_type: service_type}});
}
const [testLog] = useMutation(testLogMutation, {
onCompleted: data => {
if(data.consumingServicesTestLog.status === "success"){
if (data.consumingServicesTestLog.status === "success") {
snackActions.success("Successfully sent test message to service");
} else {
snackActions.error("No logger listening")
console.log(data.consumingServicesTestLog.error)
}
},
onError: error => {
@@ -67,118 +86,375 @@ export function ConsumingServicesTable({servicesList}){
const issueTestLog = (service_type) => {
testLog({variables: {service_type: service_type}});
}
const [openIDPMetadata, setOpenIDPMetadata] = React.useState(false);
const IDPMetadataRef = React.useRef({"container": "", "idp": ""});
const openListFilesInformation = React.useRef("");
const [webhooks, setWebhooks] = React.useState([]);
const [logging, setLogging] = React.useState([]);
const [eventing, setEventing] = React.useState([]);
const [auth, setAuth] = React.useState([]);
const [openListFilesDialog, setOpenListFilesDialog] = React.useState(false);
const [openDelete, setOpenDeleteDialog] = React.useState(false);
const deletingContainer = React.useRef({});
const [updateDeleted] = useMutation(toggleDeleteStatus, {
onCompleted: data => {
snackActions.success("Successfully updated");
},
onError: error => {
console.log(error);
snackActions.error("Failed to update: " + error.message);
}
});
const onAcceptDelete = () => {
updateDeleted({variables: {id: deletingContainer.current.id, deleted: !deletingContainer.current.deleted}})
setOpenDeleteDialog(false);
}
const adjustingDelete = (service) => {
deletingContainer.current = service;
setOpenDeleteDialog(true);
}
const getIDPMetadata = (container, idp) => {
IDPMetadataRef.current = {"container": container, "idp": idp};
setOpenIDPMetadata(true);
}
const onOpenListFilesDialog = (container) => {
openListFilesInformation.current = container;
setOpenListFilesDialog(true);
}
React.useEffect(() => {
const localWebhooks = services.filter(cur => cur.type === "webhook").sort((a, b) => a.name < b.name ? 1 : -1);
setWebhooks(localWebhooks);
const localLogging = services.filter(cur => cur.type === "logging").sort((a, b) => a.name < b.name ? 1 : -1);
setLogging(localLogging);
const localEventing = services.filter(cur => cur.type === "eventing").sort((a, b) => a.name < b.name ? 1 : -1);
const parsedLocalEventing = localEventing.map( e => {
const newSubs = e.subscriptions.map( s => {
try{
return JSON.parse(s);
}catch(error){
console.log(error);
return {name: "", description: s};
}
});
return {...e, subscriptions: newSubs}
})
setEventing(parsedLocalEventing);
const localAuth = services.filter(cur => cur.type === "auth").sort((a, b) => a.name < b.name ? 1 : -1);
const parsedLocalAuth = localAuth.map( e => {
const newSubs = e.subscriptions.map( s => {
try{
return JSON.parse(s);
}catch(error){
console.log(error);
return {name: s, type: ""};
}
});
return {...e, subscriptions: newSubs}
})
setAuth(parsedLocalAuth);
}, [services]);
return (
<React.Fragment>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main, marginBottom: "5px", marginTop: "10px", marginRight: "5px"}} variant={"elevation"}>
<Typography variant="h3" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
Services Listening for Notifications
</Typography>
</Paper>
<div style={{display: "flex"}}>
<TableContainer component={Paper} className="mythicElement" >
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(50vw)", "overflow": "scroll"}}>
<TableHead>
<TableRow>
<TableCell >Logging Category</TableCell>
<TableCell >Test Log Data</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow hover >
<TableCell>New Callback Log</TableCell>
<TableCell>
<IconButton onClick={()=>{issueTestLog("new_callback")}} size="large"> <SyncAltIcon /></IconButton>
</TableCell>
</TableRow>
<TableRow hover >
<TableCell>New Credential Log</TableCell>
<TableCell>
<IconButton onClick={()=>{issueTestLog("new_credential")}} size="large"> <SyncAltIcon /></IconButton>
</TableCell>
</TableRow>
<TableRow hover >
<TableCell>New File Log</TableCell>
<TableCell>
<IconButton onClick={()=>{issueTestLog("new_file")}} size="large"> <SyncAltIcon /></IconButton>
</TableCell>
</TableRow>
<TableRow hover>
<TableCell>New Artifact Log</TableCell>
<TableCell>
<IconButton onClick={()=>{issueTestLog("new_artifact")}} size="large"> <SyncAltIcon /></IconButton>
</TableCell>
</TableRow>
<TableRow hover>
<TableCell>New Task Log</TableCell>
<TableCell>
<IconButton onClick={()=>{issueTestLog("new_task")}} size="large"> <SyncAltIcon /></IconButton>
</TableCell>
</TableRow>
<TableRow hover >
<TableCell>New Payload Log</TableCell>
<TableCell>
<IconButton onClick={()=>{issueTestLog("new_payload")}} size="large"> <SyncAltIcon /></IconButton>
</TableCell>
</TableRow>
<TableRow hover >
<TableCell>New Keylog Log</TableCell>
<TableCell>
<IconButton onClick={()=>{issueTestLog("new_keylog")}} size="large"> <SyncAltIcon /></IconButton>
</TableCell>
</TableRow>
<TableRow hover >
<TableCell>New Response Log</TableCell>
<TableCell>
<IconButton onClick={()=>{issueTestLog("new_response")}} size="large"> <SyncAltIcon /></IconButton>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<TableContainer component={Paper} className="mythicElement" >
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(50vw)", "overflow": "scroll"}}>
<TableHead>
<TableRow>
<TableCell >Webhook Category</TableCell>
<TableCell >Test Webhook</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow hover >
<TableCell>New Feedback Webhook</TableCell>
<TableCell>
<IconButton onClick={()=>{issueTestWebook("new_feedback")}} size="large"><PublicIcon /></IconButton>
</TableCell>
</TableRow>
<TableRow hover >
<TableCell>New Callback Webhook</TableCell>
<TableCell>
<IconButton onClick={()=>{issueTestWebook("new_callback")}} size="large"><PublicIcon /></IconButton>
</TableCell>
</TableRow>
<TableRow hover>
<TableCell>New Startup Webhook</TableCell>
<TableCell>
<IconButton onClick={()=>{issueTestWebook("new_startup")}} size="large"><PublicIcon /></IconButton>
</TableCell>
</TableRow>
<TableRow hover >
<TableCell>New Alert Webhook</TableCell>
<TableCell>
<IconButton onClick={()=>{issueTestWebook("new_alert")}} size="large"><PublicIcon /></IconButton>
</TableCell>
</TableRow>
<TableRow hover >
<TableCell>New Custom Webhook</TableCell>
<TableCell>
<IconButton onClick={()=>{issueTestWebook("new_custom")}} size="large"><PublicIcon /></IconButton>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</div>
</React.Fragment>
<Paper elevation={5} style={{
backgroundColor: theme.pageHeader.main,
color: theme.pageHeaderText.main,
marginBottom: "5px",
marginLeft: "5px",
marginRight: "5px"
}} variant={"elevation"}>
<Typography variant="h3" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
Containers Consuming Events
</Typography>
</Paper>
<div style={{display: "flex", flexGrow: 1}}>
<TableContainer className="mythicElement">
<Table stickyHeader={true} size="small" style={{"tableLayout": "fixed", "overflow": "scroll"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "3rem"}}></TableCell>
<TableCell style={{width: "10rem"}}>Name</TableCell>
<TableCell style={{width: "7rem"}}>Category</TableCell>
<TableCell>Description</TableCell>
<TableCell style={{width: "4rem"}}>Manage</TableCell>
<TableCell>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{webhooks.map( (w, index) => (
<TableRow key={w.id} hover>
<MythicTableCell>
{w.deleted ? (
<IconButton onClick={() => {
adjustingDelete(w);
}} color="success" size="large">
<RestoreFromTrashOutlinedIcon/>
</IconButton>
) : (
<IconButton onClick={() => {
adjustingDelete(w);
}} color="error" size="large">
<DeleteIcon/>
</IconButton>
)}
</MythicTableCell>
<MythicTableCell>
{w.name}
{w.container_running &&
<Typography variant="body2" component="p"
style={{color: theme.palette.success.main}}>
<b>{"Online"}</b>
</Typography>
}
{!w.container_running &&
<Typography variant="body2" component="p"
style={{color: theme.palette.error.main}}>
<b>{"Offline"}</b>
</Typography>
}
</MythicTableCell>
<MythicTableCell>{w.type}</MythicTableCell>
<MythicTableCell>{w.description}</MythicTableCell>
<MythicTableCell>
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
<IconButton
color={"secondary"}
disabled={!w.container_running}
onClick={()=>{onOpenListFilesDialog(w.name);}}
size="large">
<AttachFileIcon />
</IconButton>
</MythicStyledTooltip>
</MythicTableCell>
<MythicTableCell>
{webhook_events.map(s => (
<MythicStyledTooltip title={"test webhook " + s} key={w.id + "webhook_" + s}>
<IconButton
disabled={!w.subscriptions.includes(s)}
onClick={() => {
issueTestWebhook(s)
}}
size="large">
<PublicIcon/>
</IconButton>
</MythicStyledTooltip>
))}
</MythicTableCell>
</TableRow>
))}
{logging.map(w => (
<TableRow key={w.id} hover>
<MythicTableCell>
{w.deleted ? (
<IconButton onClick={() => {
adjustingDelete(w);
}} color="success" size="large">
<RestoreFromTrashOutlinedIcon/>
</IconButton>
) : (
<IconButton onClick={() => {
adjustingDelete(w);
}} color="error" size="large">
<DeleteIcon/>
</IconButton>
)}
</MythicTableCell>
<MythicTableCell>
{w.name}
{w.container_running &&
<Typography variant="body2" component="p"
style={{color: theme.palette.success.main}}>
<b>{"Online"}</b>
</Typography>
}
{!w.container_running &&
<Typography variant="body2" component="p"
style={{color: theme.palette.error.main}}>
<b>{"Offline"}</b>
</Typography>
}
</MythicTableCell>
<MythicTableCell>{w.type}</MythicTableCell>
<MythicTableCell>{w.description}</MythicTableCell>
<MythicTableCell>
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
<IconButton
color={"secondary"}
disabled={!w.container_running}
onClick={()=>{onOpenListFilesDialog(w.name);}}
size="large">
<AttachFileIcon />
</IconButton>
</MythicStyledTooltip>
</MythicTableCell>
<MythicTableCell>
{logging_events.map(s => (
<MythicStyledTooltip title={"test logging " + s} key={w.id + "logging_" + s}>
<IconButton
disabled={!w.subscriptions.includes(s)}
onClick={() => {
issueTestLog(s)
}}
size="large">
<SyncAltIcon/>
</IconButton>
</MythicStyledTooltip>
))}
</MythicTableCell>
</TableRow>
))}
{eventing.map( (w, index) => (
<TableRow key={w.id} hover>
<MythicTableCell>
{w.deleted ? (
<IconButton onClick={() => {
adjustingDelete(w);
}} color="success" size="large">
<RestoreFromTrashOutlinedIcon/>
</IconButton>
) : (
<IconButton onClick={() => {
adjustingDelete(w);
}} color="error" size="large">
<DeleteIcon/>
</IconButton>
)}
</MythicTableCell>
<MythicTableCell>
{w.name}
{w.container_running &&
<Typography variant="body2" component="p"
style={{color: theme.palette.success.main}}>
<b>{"Online"}</b>
</Typography>
}
{!w.container_running &&
<Typography variant="body2" component="p"
style={{color: theme.palette.error.main}}>
<b>{"Offline"}</b>
</Typography>
}
</MythicTableCell>
<MythicTableCell>{w.type}</MythicTableCell>
<MythicTableCell>{w.description}</MythicTableCell>
<MythicTableCell>
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
<IconButton
color={"secondary"}
disabled={!w.container_running}
onClick={()=>{onOpenListFilesDialog(w.name);}}
size="large">
<AttachFileIcon />
</IconButton>
</MythicStyledTooltip>
</MythicTableCell>
<MythicTableCell>
<Table>
<TableHead>
<TableRow>
<TableCell>Function</TableCell>
<TableCell>Description</TableCell>
</TableRow>
</TableHead>
<TableBody>
{w.subscriptions.map(s => (
<TableRow key={s.name} hover>
<MythicTableCell><b>{s.name}</b></MythicTableCell>
<MythicTableCell>{s.description}</MythicTableCell>
</TableRow>
))}
</TableBody>
</Table>
</MythicTableCell>
</TableRow>
))}
{auth.map( (w, index) => (
<TableRow key={w.id} hover>
<MythicTableCell>
{w.deleted ? (
<IconButton onClick={() => {
adjustingDelete(w);
}} color="success" size="large">
<RestoreFromTrashOutlinedIcon/>
</IconButton>
) : (
<IconButton onClick={() => {
adjustingDelete(w);
}} color="error" size="large">
<DeleteIcon/>
</IconButton>
)}
</MythicTableCell>
<MythicTableCell>
{w.name}
{w.container_running &&
<Typography variant="body2" component="p"
style={{color: theme.palette.success.main}}>
<b>{"Online"}</b>
</Typography>
}
{!w.container_running &&
<Typography variant="body2" component="p"
style={{color: theme.palette.error.main}}>
<b>{"Offline"}</b>
</Typography>
}
</MythicTableCell>
<MythicTableCell>{w.type}</MythicTableCell>
<MythicTableCell>{w.description}</MythicTableCell>
<MythicTableCell>
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
<IconButton
color={"secondary"}
disabled={!w.container_running}
onClick={()=>{onOpenListFilesDialog(w.name);}}
size="large">
<AttachFileIcon />
</IconButton>
</MythicStyledTooltip>
</MythicTableCell>
<MythicTableCell>
{w.subscriptions.map(s => (
<Typography key={s.name + s.type + w.name} style={{display: "block"}}>
<MythicStyledTooltip title={"Fetch Container Metadata"} >
<IconButton onClick={() => getIDPMetadata(w.name, s.name)} >
<PermIdentityTwoToneIcon />
</IconButton>
</MythicStyledTooltip>
{s.name}
</Typography>
))}
</MythicTableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
{openDelete &&
<MythicConfirmDialog onClose={() => { setOpenDeleteDialog(false); }}
onSubmit={onAcceptDelete}
open={openDelete}
acceptText={deletingContainer.current.deleted ? "Restore" : "Remove"}
acceptColor={deletingContainer.current.deleted ? "success" : "error"}/>
}
{openIDPMetadata &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openIDPMetadata}
onClose={()=>{setOpenIDPMetadata(false);}}
innerDialog={<ConsumingServicesGetIDPMetadataDialog
container={IDPMetadataRef.current.container}
idp={IDPMetadataRef.current.idp}
onClose={()=>{setOpenIDPMetadata(false);}} />}
/>
}
{openListFilesDialog &&
<MythicDialog fullWidth={true} maxWidth="md" open={openListFilesDialog}
onClose={()=>{setOpenListFilesDialog(false);}}
innerDialog={<C2ProfileListFilesDialog container_name={openListFilesInformation.current}
onClose={()=>{setOpenListFilesDialog(false);}} />}
/>
}
</div>
</React.Fragment>
);
}
@@ -60,15 +60,37 @@ export function CreatePayload(props){
}, [noOperation])
return (
<div >
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%"}} >
<Stepper activeStep={activeStep} alternativeLabel style={{marginTop: "10px"}}>
{steps.map((label, index) => (
<Step key={label}>
<Step key={label}
sx={{
'& .MuiStepLabel-root .Mui-completed': {
color: 'success.main', // circle color (COMPLETED)
},
'& .MuiStepLabel-label.Mui-completed.MuiStepLabel-alternativeLabel':
{
color: 'grey.500', // Just text label (COMPLETED)
},
'& .MuiStepLabel-root .Mui-active': {
color: 'info.main', // circle color (ACTIVE)
},
'& .MuiStepLabel-label.Mui-active.MuiStepLabel-alternativeLabel':
{
fontWeight: "bold", // Just text label (ACTIVE)
color: ''
},
'& .MuiStepLabel-root .Mui-active .MuiStepIcon-text': {
fill: 'black', // circle's number (ACTIVE)
},
}}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
{getStepContent(activeStep)}
<div style={{display: "flex", flexDirection: "column", flexGrow: 1, overflowY: 'auto'}}>
{getStepContent(activeStep)}
</div>
</div>
);
}
@@ -5,23 +5,17 @@ import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import { CreatePayloadParameter } from './CreatePayloadParameter';
import Typography from '@mui/material/Typography';
export function CreatePayloadBuildParametersTable(props){
return (
<TableContainer component={Paper} className="mythicElement">
<Typography variant="h4" align="left" id="SettingsTable" component="div"
style={{"display": "inline-block", "float": "left", "marginLeft": "10px"}}>
Build Parameters
</Typography>
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
<TableContainer className="mythicElement">
<Table stickyHeader={true} size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "20%"}}>Build Parameter</TableCell>
<TableCell style={{width: "30%"}}>Build Parameter</TableCell>
<TableCell>Value</TableCell>
</TableRow>
</TableHead>
@@ -5,7 +5,6 @@ import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import { CreatePayloadParameter } from './CreatePayloadParameter';
@@ -23,14 +23,15 @@ export function CreatePayloadNavigationButtons(props){
Back
</Button>
<Button
variant="contained"
color={props.last ? "success" : "primary"}
onClick={props.finished}
disabled={disabledButtons}
variant="contained"
color={props.last ? "success" : "primary"}
onClick={props.finished}
disabled={disabledButtons}
>
{props.last ? 'Create Payload' : 'Next'}
{props.last ? props.showExtraOptions ? 'Create Payload Again' : 'Create Payload' : 'Next'}
</Button>
{props.last &&
{props.last && props.showExtraOptions &&
<React.Fragment>
<Button
variant="contained"
@@ -40,15 +41,19 @@ export function CreatePayloadNavigationButtons(props){
>
Start Over
</Button>
<Button
variant="contained"
color="info"
component={Link}
style={{marginLeft: "10px"}}
to={"/new/createwrapper"}
>
Create Wrapper
</Button>
{props.showExtraOptions &&
<Button
variant="contained"
color="info"
component={Link}
style={{marginLeft: "10px"}}
to={"/new/createwrapper"}
>
Go To Create Wrapper
</Button>
}
</React.Fragment>
}
@@ -16,7 +16,6 @@ import { DesktopDatePicker } from '@mui/x-date-pickers/DesktopDatePicker';
import dayjs from 'dayjs';
import Switch from '@mui/material/Switch';
import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip';
import Paper from '@mui/material/Paper';
import MythicStyledTableCell from '../../MythicComponents/MythicTableCell';
import {Typography} from '@mui/material';
import {MythicFileContext} from "../../MythicComponents/MythicFileContext";
@@ -26,6 +25,7 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
const [value, setValue] = React.useState("");
const [valueNum, setValueNum] = React.useState(0);
const [multiValue, setMultiValue] = React.useState([]);
const [chooseOneCustomValue, setChooseOneCustomValue] = React.useState("");
const [dictValue, setDictValue] = React.useState([]);
const [dictOptions, setDictOptions] = React.useState([]);
const [dictSelectOptions, setDictSelectOptions] = React.useState([]);
@@ -35,6 +35,7 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
const [arrayValue, setArrayValue] = React.useState([""]);
const [typedArrayValue, setTypedArrayValue] = React.useState([]);
const [fileValue, setFileValue] = React.useState({name: ""});
const [fileMultValue, setFileMultValue] = React.useState([]);
const submitDictChange = (list) => {
onChange(name, list, false);
};
@@ -42,10 +43,17 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
setFileValue({name: evt.target.files[0].name});
onChange(name, evt.target.files[0]);
}
const onFileMultChange = (evt) => {
setFileMultValue([...evt.target.files]);
onChange(name, [...evt.target.files]);
}
useEffect( () => {
if(parameter_type === "ChooseOne"){
if(parameter_type === "ChooseOne" || parameter_type === "ChooseOneCustom"){
setValue(trackedValue);
setChooseOptions(choices);
setChooseOptions(choices);
if(!choices.includes(trackedValue)){
setChooseOneCustomValue(trackedValue);
}
}else if(parameter_type === "Number"){
setValueNum(trackedValue);
}else if(parameter_type === "String"){
@@ -54,11 +62,13 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
setMultiValue(trackedValue);
setChooseOptions(choices);
}else if(parameter_type === "File") {
if(typeof trackedValue === "string"){
if (typeof trackedValue === "string") {
setFileValue({name: trackedValue, legacy: trackedValue !== ""});
} else {
setFileValue(trackedValue);
}
} else if(parameter_type === "FileMultiple"){
setFileMultValue(trackedValue);
}else if(parameter_type === "Date"){
setDateValue(dayjs(trackedValue));
onChange(name, trackedValue, "");
@@ -105,7 +115,15 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
console.log("hit an unknown parameter type")
}
}, [default_value, parameter_type, name]);
const onChangeTextChooseOneCustom = (name, newValue, error) => {
setChooseOneCustomValue(newValue);
if(newValue === ""){
onChange(name, value, error);
} else {
onChange(name, newValue, error);
}
}
const onChangeValue = (evt) => {
setValue(evt.target.value);
onChange(name, evt.target.value, false);
@@ -253,7 +271,6 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
new_values[0] = new_values[0].slice(values[index].length)
values = [...values.slice(0, index+1), ...new_values, ...values.slice(index+1)];
}
console.log(values);
}else{
values[index] = value;
}
@@ -305,6 +322,23 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
</Grid>
</LocalizationProvider>
);
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>
))
}
</>
)
case "File":
return (
<>
@@ -333,6 +367,34 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
</Select>
</FormControl>
);
case "ChooseOneCustom":
return (
<React.Fragment>
<div style={{width: "100%", display: "flex", alignItems: "center"}}>
<FormControl style={{}}>
<Select
native
multiple={false}
disabled={chooseOneCustomValue !== ""}
value={value}
onChange={onChangeValue}
input={<Input />}
>
{
chooseOptions.map((opt, i) => (
<option key={name + i} value={opt}>{opt}</option>
))
}
</Select>
</FormControl>
OR
<MythicTextField name={name} required={required} placeholder={"Custom Value"} value={chooseOneCustomValue} multiline={true} maxRows={5}
onChange={onChangeTextChooseOneCustom} display="inline-block"
/>
</div>
</React.Fragment>
)
case "ChooseMultiple":
return (
<FormControl>
@@ -1,8 +1,6 @@
import React from 'react';
import {useQuery, gql} from '@apollo/client';
import CircularProgress from '@mui/material/CircularProgress';
import FormControl from '@mui/material/FormControl';
import FormHelperText from '@mui/material/FormHelperText';
import Select from '@mui/material/Select';
import { CreatePayloadNavigationButtons} from './CreatePayloadNavigationButtons';
import Typography from '@mui/material/Typography';
@@ -49,7 +47,6 @@ export function Step1SelectOS(props){
snackActions.error(data.message)
}
});
if (loading) {
return <div><CircularProgress /></div>;
}
@@ -66,26 +63,27 @@ export function Step1SelectOS(props){
props.canceled();
}
return (
<div >
<Typography variant="h3" align="left" id="selectospage" component="div"
style={{ "marginLeft": "10px"}}>
Select Target Operating System
</Typography> <br/>
<FormControl>
<Select
value={os}
onChange={evt => setOS(evt.target.value)}
>
{
osOptions.map((opt) => (
<MenuItem key={"step1" + opt} value={opt}>{opt}</MenuItem>
))
}
</Select>
<FormHelperText>Target Operating System</FormHelperText>
</FormControl><br/><br/>
<CreatePayloadNavigationButtons first={props.first} last={props.last} canceled={canceled} finished={finished} />
<div style={{height: "100%", display: "flex", flexDirection: "column"}}>
<Typography variant="h3" align="left" id="selectospage" component="div"
style={{"marginLeft": "10px"}}>
Select Target Operating System
</Typography> <br/>
<div style={{flexGrow: 1, width: "100%", display: "flex", flexDirection: "column"}}>
<Select
value={os}
onChange={evt => setOS(evt.target.value)}
>
{
osOptions.map((opt) => (
<MenuItem key={"step1" + opt} value={opt}>{opt}</MenuItem>
))
}
</Select>
</div>
<CreatePayloadNavigationButtons first={props.first} last={props.last} canceled={canceled}
finished={finished}/>
<br/><br/>
</div>
);
}
@@ -10,7 +10,7 @@ import MenuItem from '@mui/material/MenuItem';
const GET_Payload_Types = gql`
query getPayloadTypesBuildParametersQuery($os: jsonb!) {
payloadtype(where: {supported_os: {_contains: $os}, deleted: {_eq: false}, wrapper: {_eq: false}}, order_by: {name: asc}) {
payloadtype(where: {supported_os: {_contains: $os}, deleted: {_eq: false}, wrapper: {_eq: false}, _or: [ {agent_type: {_eq: "agent"}}, {agent_type: {_eq: "service"}}] }, order_by: {name: asc}) {
name
id
file_extension
@@ -33,8 +33,7 @@ query getPayloadTypesBuildParametersQuery($os: jsonb!) {
export const getDefaultValueForType = (parameter) => {
// all default values will be strings, so convert them
if(parameter.randomize && parameter.format_string !== ""){
const random = new RandExp(parameter.format_string).gen();
return random;
return new RandExp(parameter.format_string).gen();
}
switch (parameter.parameter_type) {
case "String":
@@ -44,6 +43,8 @@ export const getDefaultValueForType = (parameter) => {
return parameter.default_value * 1;
case "ChooseOne":
return parameter.default_value;
case "ChooseOneCustom":
return parameter.default_value;
case "ChooseMultiple":
// default_value will be a json string of an array
return JSON.parse(parameter.default_value);
@@ -65,6 +66,8 @@ export const getDefaultValueForType = (parameter) => {
return {...c, value: c.default_value}
});
}
case "FileMultiple":
return [];
case "File":
return {name: ""};
case "Date":
@@ -200,7 +203,7 @@ export function Step2SelectPayloadType(props){
return <div>Error! {error.message}</div>;
}
return (
<div >
<div style={{height: "100%", display: "flex", flexDirection: "column"}}>
<Typography variant="h3" align="left" id="selectospage" component="div"
style={{"marginLeft": "10px"}}>
Select Target Payload Type
@@ -215,7 +218,9 @@ export function Step2SelectPayloadType(props){
))
}
</Select><br/>
<CreatePayloadBuildParametersTable onChange={onChange} buildParameters={payloadTypeParameters} />
<div style={{display: "flex", flexGrow: 1, overflowY: "auto"}}>
<CreatePayloadBuildParametersTable onChange={onChange} buildParameters={payloadTypeParameters} />
</div>
<CreatePayloadNavigationButtons first={props.first} last={props.last} canceled={canceled} finished={finished} />
<br/><br/>
</div>
@@ -37,7 +37,7 @@ export function Step3SelectCommands(props){
if(!props.buildOptions["supports_dynamic_loading"]){
const allCommands = data.command.map( c => {
return {...c, selected: true, disabled: true, reason: "Agent doesn't support dynamic loading"}
return {...c, selected: true, disabled: true, reason: "Always included because agent doesn't support dynamic loading"}
});
setCommandOptions(allCommands);
}else{
@@ -126,19 +126,16 @@ export function Step3SelectCommands(props){
props.canceled();
}
return (
<div >
<Typography variant="h3" align="left" id="selectcommands" component="div"
style={{ "marginLeft": "10px"}}>
Build Commands Into Agent
</Typography> <br/>
<CommandTransferSelect commands={commandOptions} payload_type={props.buildOptions["payload_type"]} first={props.first} last={props.last}
canceled={canceled} finished={finished}/>
<MythicConfirmDialog open={openConfirmDialog}
title={"No exit command selected, continue?"}
onClose={() => setOpenConfirmDialog(false)}
acceptText="Accept"
onSubmit={acceptConfirm} />
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%", marginTop: "20px"}}>
<CommandTransferSelect commands={commandOptions} payload_type={props.buildOptions["payload_type"]} first={props.first} last={props.last}
canceled={canceled} finished={finished}/>
{openConfirmDialog &&
<MythicConfirmDialog open={openConfirmDialog}
title={"No exit command selected, continue?"}
onClose={() => setOpenConfirmDialog(false)}
acceptText="Accept"
onSubmit={acceptConfirm} />
}
</div>
);
}
@@ -251,133 +248,140 @@ function CommandTransferSelect(props) {
}
}
const customList = (title, items) => (
<Paper style={{width:"100%", height: "calc(40vh)", overflow: "auto"}} elevation={5}>
<CardHeader
title={title}
/>
<Paper style={{width:"100%", height: "calc(40vh)", display: "flex", flexDirection: "column", overflowY: "auto"}} elevation={5}>
<CardHeader title={title} />
<StyledDivider className={classes.divider}/>
<List dense component="div" role="list" style={{padding:0}}>
{items.map((valueObj) => {
const value = valueObj["cmd"];
const labelId = `transfer-list-item-${value}-label`;
return (
<div onMouseEnter={setHoveredData} key={'commandtransfer' + value}>
<ListItem style={{padding:0}} disabled={valueObj["disabled"]}
key={value} role="listitem" button onClick={handleToggle(valueObj)}
>
<ListItemIcon>
<Checkbox
disabled={valueObj["disabled"]}
checked={checked.findIndex( (element) => element["cmd"] === value) !== -1}
tabIndex={-1}
disableRipple
inputProps={{ 'aria-labelledby': labelId }}
/>
</ListItemIcon>
<ListItemText id={labelId} primary={value} />
</ListItem>
</div>
);
})}
<ListItem />
</List>
<div style={{display: "flex", flexGrow: 1, overflowY: "auto", width: "100%"}}>
<List dense component="div" role="list" style={{padding:0, width: "100%", overflow: "auto"}}>
{items.map((valueObj) => {
const value = valueObj["cmd"];
const labelId = `transfer-list-item-${value}-label`;
return (
<div onMouseEnter={setHoveredData} key={'commandtransfer' + value}>
<ListItem style={{padding:0}} disabled={valueObj["disabled"]}
key={value} role="listitem" button onClick={handleToggle(valueObj)}
>
<ListItemIcon>
<Checkbox
disabled={valueObj["disabled"]}
checked={checked.findIndex( (element) => element["cmd"] === value) !== -1}
tabIndex={-1}
disableRipple
inputProps={{ 'aria-labelledby': labelId }}
/>
</ListItemIcon>
<ListItemText id={labelId} primary={value} />
</ListItem>
</div>
);
})}
<ListItem />
</List>
</div>
</Paper>
);
const finished = () => {
props.finished(right);
}
return (
<>
<Grid container spacing={2} justifyContent="center" alignItems="center" className={classes.root}>
<Grid item xs={5}>
{customList("Available Commands", left)}
</Grid>
<Grid item>
<Grid container direction="column" alignItems="center">
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleAllRight}
disabled={left.length === 0}
aria-label="move all right"
>
&gt;&gt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleCheckedRight}
disabled={leftChecked.length === 0}
aria-label="move selected right"
>
&gt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleCheckedLeft}
disabled={rightChecked.length === 0}
aria-label="move selected left"
>
&lt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleAllLeft}
disabled={right.length === 0}
aria-label="move all left"
>
&lt;&lt;
</StyledButton>
</Grid>
</Grid>
<Grid item xs={5}>
{customList("Commands Included", right)}
</Grid>
</Grid>
<Grid container justifyContent="center" alignItems="flex-start" className={classes.root}>
<Grid item xs={12} style={{height: "100%", marginBottom: "10px"}}>
{hoveredCommand["cmd"] !== undefined &&
<Paper className={classes.paper} style={{width: "100%"}} elevation={5}>
<CardHeader
title={
<React.Fragment>
{hoveredCommand["cmd"]}
<Button variant="contained" color="primary" href={"/docs/agents/" + props.payload_type + "/commands/" + hoveredCommand["cmd"]}
style={{marginLeft: "10px", float: "right"}} target="_blank">Documentation
</Button>
</React.Fragment>
}
/>
<StyledDivider classes={{root: classes.divider}}/>
{hoveredCommand["reason"] !== "" ? (
<Typography variant="body1" align="left" component="div"
style={{ "marginLeft": "10px"}}><b>{hoveredCommand["disabled"] ? ("Disabled Reason: ") : ("Information: ")} </b>{hoveredCommand["reason"]}
</Typography>
) : null}
<br/>
<Typography align="left" component="div"
style={{ "marginLeft": "10px"}}><b>Commandline Help: </b>{hoveredCommand["help_cmd"]}
</Typography>
<Typography align="left" component="div"
style={{ "marginLeft": "10px"}}><b>Needs Admin Permissions: </b>{hoveredCommand["needs_admin"] ? "True": "False"}
</Typography>
<Typography align="left" component="div"
style={{ "marginLeft": "10px"}}><b>Description: </b>{hoveredCommand["description"]}
</Typography>
</Paper>
}
</Grid>
</Grid>
<CreatePayloadNavigationButtons first={props.first} last={props.last} canceled={props.canceled} finished={finished} />
</>
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%"}}>
<div style={{flexGrow: 1}}>
<Grid container spacing={2} justifyContent="center" alignItems="center" className={classes.root}>
<Grid item xs={5}>
{customList("Available Commands", left)}
</Grid>
<Grid item>
<Grid container direction="column" alignItems="center">
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleAllRight}
disabled={left.length === 0}
aria-label="move all right"
>
&gt;&gt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleCheckedRight}
disabled={leftChecked.length === 0}
aria-label="move selected right"
>
&gt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleCheckedLeft}
disabled={rightChecked.length === 0}
aria-label="move selected left"
>
&lt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleAllLeft}
disabled={right.length === 0}
aria-label="move all left"
>
&lt;&lt;
</StyledButton>
</Grid>
</Grid>
<Grid item xs={5}>
{customList("Commands Included", right)}
</Grid>
</Grid>
<Grid container justifyContent="center" alignItems="flex-start" className={classes.root}>
<Grid item xs={12} style={{height: "100%", marginBottom: "10px"}}>
{hoveredCommand["cmd"] !== undefined &&
<Paper className={classes.paper} style={{width: "100%"}} elevation={5}>
<CardHeader
title={
<React.Fragment>
{hoveredCommand["cmd"]}
<Button variant="contained" color="primary"
href={"/docs/agents/" + props.payload_type + "/commands/" + hoveredCommand["cmd"]}
style={{marginLeft: "10px", float: "right"}} target="_blank">Documentation
</Button>
</React.Fragment>
}
/>
<StyledDivider classes={{root: classes.divider}}/>
{hoveredCommand["reason"] !== "" ? (
<Typography variant="body1" align="left" component="div"
style={{"marginLeft": "10px"}}><b>{hoveredCommand["disabled"] ? ("Disabled Reason: ") : ("Information: ")} </b>{hoveredCommand["reason"]}
</Typography>
) : null}
<br/>
<Typography align="left" component="div"
style={{"marginLeft": "10px"}}><b>Commandline
Help: </b>{hoveredCommand["help_cmd"]}
</Typography>
<Typography align="left" component="div"
style={{"marginLeft": "10px"}}><b>Needs Admin
Permissions: </b>{hoveredCommand["needs_admin"] ? "True" : "False"}
</Typography>
<Typography align="left" component="div"
style={{"marginLeft": "10px"}}><b>Description: </b>{hoveredCommand["description"]}
</Typography>
</Paper>
}
</Grid>
</Grid>
</div>
<CreatePayloadNavigationButtons first={props.first} last={props.last} canceled={props.canceled}
finished={finished}/>
<br/><br/>
</div>
);
}
@@ -14,10 +14,8 @@ import {getDefaultValueForType, getDefaultChoices} from './Step2SelectPayloadTyp
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import MythicStyledTableCell from '../../MythicComponents/MythicTableCell';
import {MythicConfirmDialog} from "../../MythicComponents/MythicConfirmDialog";
@@ -194,7 +192,7 @@ export function Step4C2Profiles(props){
onCompleted: (data) => {
const updates = data.c2profileparametersinstance.map( (cur) => {
let inst = {...cur, ...cur.c2profileparameter};
if(inst.parameter_type === "Array" || inst.parameter_type === "ChooseMultiple" || inst.parameter_type === "TypedArray"){
if(inst.parameter_type === "Array" || inst.parameter_type === "ChooseMultiple" || inst.parameter_type === "TypedArray" || inst.parameter_type === "FileMultiple"){
try{
inst["value"] = JSON.parse(inst["value"]);
}catch(error){
@@ -305,79 +303,85 @@ export function Step4C2Profiles(props){
return <div>Error! {error.message}</div>;
}
return (
<div >
<Typography variant="h3" align="left" id="selectc2profiles" component="div"
style={{"marginLeft": "10px"}}>
Select C2 Profiles
</Typography>
<TableContainer component={Paper} className="mythicElement">
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
<div style={{paddingTop: "20px", display: "flex", flexDirection: "column", height: "100%", width: "100%"}}>
<div style={{flexGrow: 1, width: "100%", overflowY: "scroll"}}>
<Table stickyHeader={true} size="small" style={{"maxWidth": "100%",}}>
<TableHead>
<TableRow>
<TableCell style={{width: "4rem"}}>Include?</TableCell>
<TableCell >C2 Name</TableCell>
<TableCell >Pre-created Instances</TableCell>
<TableCell >Description</TableCell>
<TableCell>C2 Name</TableCell>
<TableCell>Pre-created Instances</TableCell>
<TableCell>Description</TableCell>
</TableRow>
</TableHead>
{
c2Profiles.map( (c2) => (
<TableBody key={"step4c2tablerow" + c2.id}>
<TableRow hover>
<MythicStyledTableCell>
c2Profiles.map((c2) => (
<TableBody key={"step4c2tablerow" + c2.id}>
<TableRow hover>
<MythicStyledTableCell>
<Switch
checked={c2.selected}
onChange={evt => toggleC2Selection(evt, c2, !c2.selected)}
inputProps={{ 'aria-label': 'primary checkbox' }}
inputProps={{'aria-label': 'primary checkbox'}}
name="active"
/>
</MythicStyledTableCell>
<MythicStyledTableCell>
{c2.name}
</MythicStyledTableCell>
<MythicStyledTableCell>
{c2.c2profileparametersinstances.length > 0 ? (
</MythicStyledTableCell>
<MythicStyledTableCell>
{c2.name}
</MythicStyledTableCell>
<MythicStyledTableCell>
{c2.c2profileparametersinstances.length > 0 ? (
<Select
style={{width: "100%", marginBottom: "5px", marginTop:"5px"}}
value={c2.selected_instance}
//label="Select an Existing Instance"
onChange={evt => onChangeCreatedInstanceName(evt, c2)}
style={{width: "100%", marginBottom: "5px", marginTop: "5px"}}
value={c2.selected_instance}
//label="Select an Existing Instance"
onChange={evt => onChangeCreatedInstanceName(evt, c2)}
>
<MenuItem key={"buildparamopt" + "-1"} value={"None"}>None</MenuItem>
{
c2.c2profileparametersinstances.map((opt, i) => (
<MenuItem key={"buildparamopt" + i} value={opt.instance_name}>{opt.instance_name}</MenuItem>
))
}
<MenuItem key={"buildparamopt" + "-1"}
value={"None"}>None</MenuItem>
{
c2.c2profileparametersinstances.map((opt, i) => (
<MenuItem key={"buildparamopt" + i}
value={opt.instance_name}>{opt.instance_name}</MenuItem>
))
}
</Select>
) : null}
</MythicStyledTableCell>
<MythicStyledTableCell>
<Typography variant="body1" align="left" id="selectc2profiles" component="div" key={"step4desc" + c2.id}
style={{"marginLeft": "10px"}}>
{c2.description}
</Typography>
</MythicStyledTableCell>
</TableRow>
{ c2.selected ? (
) : null}
</MythicStyledTableCell>
<MythicStyledTableCell>
<Typography variant="body1" align="left" id="selectc2profiles"
component="div" key={"step4desc" + c2.id}
style={{"marginLeft": "10px"}}>
{c2.description}
</Typography>
</MythicStyledTableCell>
</TableRow>
{c2.selected ? (
<TableRow><MythicStyledTableCell colSpan={4}>
<CreatePayloadC2ProfileParametersTable key={"step4table" + c2.id} returnAllDictValues={false} {...c2} onChange={updateC2Parameter} />
<CreatePayloadC2ProfileParametersTable key={"step4table" + c2.id}
returnAllDictValues={false} {...c2}
onChange={updateC2Parameter}/>
</MythicStyledTableCell></TableRow>
):null
) : null
}
</TableBody>
</TableBody>
))
}
</Table>
</TableContainer>
<MythicConfirmDialog open={openConfirmDialog}
title={"No C2 Profiles selected, continue?"}
onClose={() => setOpenConfirmDialog(false)}
acceptText="Accept"
onSubmit={acceptConfirm} />
</div>
{openConfirmDialog &&
<MythicConfirmDialog open={openConfirmDialog}
title={"No C2 Profiles selected, continue?"}
onClose={() => setOpenConfirmDialog(false)}
acceptText="Accept"
onSubmit={acceptConfirm} />
}
<br/>
<CreatePayloadNavigationButtons first={props.first} last={props.last} canceled={canceled} finished={finished} />
<br/><br/>
@@ -22,6 +22,7 @@ export function Step5Build(props){
const [description, setDescription] = React.useState("");
const [startSubscription, setStartSubscription] = React.useState(false);
const [subscriptionID, setSubscriptionID] = React.useState("");
const [showExtraOptions, setShowExtraOptions] = React.useState(false);
const [createPayloadMutation] = useMutation(create_payload, {
update: (cache, {data}) => {
if(data.createPayload.status === "success"){
@@ -30,6 +31,7 @@ export function Step5Build(props){
if(!startSubscription){
setStartSubscription(true);
}
setShowExtraOptions(true);
}else{
snackActions.error(data.createPayload.error);
}
@@ -38,6 +40,7 @@ export function Step5Build(props){
snackActions.error("Failed to create Payload. Do you have an active operation set?")
}
});
useEffect( () => {
if(props.buildOptions[1]["file_extension"] !== ""){
setFilename(props.buildOptions[1]["payload_type"] + "." + props.buildOptions[1]["file_extension"]);
@@ -65,18 +68,37 @@ export function Step5Build(props){
}, {});
buildParameters.push({name: param.name, value: newDict});
} else if (param.parameter_type === "File") {
if(typeof param.value === "string"){
if (typeof param.value === "string") {
buildParameters.push({name: param.name, value: param.value});
} else {
const newUUID = await UploadTaskFile(param.value, "Uploaded as build parameter for " + filename);
if (newUUID) {
buildParameters.push({name: param.name, value: newUUID});
if (newUUID !== "Missing file in form") {
buildParameters.push({name: param.name, value: newUUID});
}
} else {
snackActions.error("Failed to upload files")
return;
}
}
}else if(param.parameter_type === "FileMultiple"){
let fileMultipleValues = [];
for(let j = 0; j < param.value.length; j++){
if (typeof param.value[j] === "string") {
fileMultipleValues.push(param.value[j]);
} else {
const newUUID = await UploadTaskFile(param.value[j], "Uploaded as build parameter for " + filename);
if (newUUID) {
if (newUUID !== "Missing file in form") {
fileMultipleValues.push(newUUID);
}
} else {
snackActions.error("Failed to upload files")
return;
}
}
}
buildParameters.push({name: param.name, value: fileMultipleValues});
} else {
buildParameters.push({name: param.name, value: param.value});
}
@@ -96,17 +118,41 @@ export function Step5Build(props){
}, {});
instanceParam = {...instanceParam, [param.name]: newDict};
} else if (param.parameter_type === "File") {
if(typeof param.value === "string"){
if (typeof param.value === "string") {
instanceParam = {...instanceParam, [param.name]: param.value};
} else {
const newUUID = await UploadTaskFile(param.value, "Uploaded as c2 parameter for " + filename);
if (newUUID) {
instanceParam = {...instanceParam, [param.name]: newUUID};
if (newUUID !== "Missing file in form") {
instanceParam = {...instanceParam, [param.name]: newUUID};
} else {
snackActions.error("Failed to upload files, missing file")
}
} else {
snackActions.error("Failed to upload files")
return;
}
}
}else if(param.parameter_type === "FileMultiple"){
let fileMultipleValues = [];
for(let j = 0; j < param.value.length; j++){
if (typeof param.value[j] === "string") {
fileMultipleValues.push(param.value[j]);
} else {
const newUUID = await UploadTaskFile(param.value[j], "Uploaded as c2 parameter for " + filename);
if (newUUID) {
if (newUUID !== "Missing file in form") {
fileMultipleValues.push(newUUID);
} else {
snackActions.error("Failed to upload files, missing file")
}
} else {
snackActions.error("Failed to upload files")
return;
}
}
}
instanceParam = {...instanceParam, [param.name]: fileMultipleValues};
} else {
instanceParam = {...instanceParam, [param.name]: param.value};
//return {...prev, [param.name]: param.value}
@@ -136,18 +182,24 @@ export function Step5Build(props){
}
return (
<div >
<Typography variant="h3" align="left" id="selectc2profiles" component="div"
style={{"marginLeft": "10px"}}>
Payload Review
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%"}}>
<Typography variant="h3" align="left" id="selectc2profiles" component="div"
style={{"marginLeft": "10px"}}>
Payload Review
</Typography>
<br/>
<MythicTextField onEnter={finished} autoFocus={true} required={false} placeholder={"Filename"} value={filename} multiline={false} onChange={onChangeFilename} display="inline-block"/>
<MythicTextField onEnter={finished} required={false} placeholder={"description"} value={description} multiline={false} onChange={onChangeDescription} display="inline-block"/>
<CreatePayloadNavigationButtons first={props.first} last={props.last} canceled={canceled} finished={finished} startOver={props.startOver} />
<div style={{display: "flex", flexDirection: "column", flexGrow: 1}}>
<MythicTextField onEnter={finished} autoFocus={true} required={false} placeholder={"Filename"}
value={filename} multiline={false} onChange={onChangeFilename} display="inline-block"/>
<MythicTextField onEnter={finished} required={false} placeholder={"description"} value={description}
multiline={false} onChange={onChangeDescription} display="inline-block"/>
</div>
<CreatePayloadNavigationButtons first={props.first} last={props.last} canceled={canceled}
finished={finished} startOver={props.startOver} showExtraOptions={showExtraOptions}/>
<br/><br/>
{startSubscription && <PayloadSubscriptionNotification me={props.me} subscriptionID={subscriptionID} fromNow={fromNow}/>}
{startSubscription &&
<PayloadSubscriptionNotification me={props.me} subscriptionID={subscriptionID} fromNow={fromNow}/>}
</div>
);
}
@@ -58,15 +58,36 @@ export function CreatePayloadWrapper(props){
}, [noOperation]);
return (
<div >
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%"}}>
<Stepper activeStep={activeStep} alternativeLabel style={{marginTop: "10px"}}>
{steps.map((label, index) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
<Step key={label} sx={{
'& .MuiStepLabel-root .Mui-completed': {
color: 'success.main', // circle color (COMPLETED)
},
'& .MuiStepLabel-label.Mui-completed.MuiStepLabel-alternativeLabel':
{
color: 'grey.500', // Just text label (COMPLETED)
},
'& .MuiStepLabel-root .Mui-active': {
color: 'info.main', // circle color (ACTIVE)
},
'& .MuiStepLabel-label.Mui-active.MuiStepLabel-alternativeLabel':
{
fontWeight: "bold", // Just text label (ACTIVE)
color: ''
},
'& .MuiStepLabel-root .Mui-active .MuiStepIcon-text': {
fill: 'black', // circle's number (ACTIVE)
},
}}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
{getStepContent(activeStep)}
</Stepper>
<div style={{display: "flex", flexDirection: "column", flexGrow: 1, overflowY: 'auto'}}>
{getStepContent(activeStep)}
</div>
</div>
);
}
@@ -5,23 +5,17 @@ import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import {CreatePayloadParameter} from '../CreatePayload/CreatePayloadParameter';
import Typography from '@mui/material/Typography';
export function CreatePayloadBuildParametersTable(props){
return (
<TableContainer component={Paper} className="mythicElement">
<Typography variant="h4" align="left" id="SettingsTable" component="div"
style={{"display": "inline-block", "float": "left", "marginLeft": "10px"}}>
Build Parameters
</Typography>
<TableContainer className="mythicElement">
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "20%"}}>Build Parameter</TableCell>
<TableCell style={{width: "30%"}}>Build Parameter</TableCell>
<TableCell>Value</TableCell>
</TableRow>
</TableHead>
@@ -16,7 +16,7 @@ export function CreatePayloadNavigationButtons(props){
<div >
<Button
disabled={props.first}
color="primary"
color="secondary"
onClick={props.canceled}
>
Back
@@ -27,9 +27,9 @@ export function CreatePayloadNavigationButtons(props){
onClick={props.finished}
disabled={props.disableNext || disabledButtons}
>
{props.last ? 'Create Wrapped Payload' : 'Next'}
{props.last ? props.showExtraOptions ? 'Create Payload Again' : 'Create Payload' : 'Next'}
</Button>
{props.last &&
{props.last && props.showExtraOptions &&
<React.Fragment>
<Button
variant="contained"
@@ -39,6 +39,7 @@ export function CreatePayloadNavigationButtons(props){
>
Start Over
</Button>
{props.showExtraOptions &&
<Button
variant="contained"
color="info"
@@ -46,8 +47,9 @@ export function CreatePayloadNavigationButtons(props){
style={{marginLeft: "10px"}}
to={"/new/createpayload"}
>
Create Another Payload
Go To Create Another Base Payload
</Button>
}
</React.Fragment>
}
@@ -1,8 +1,7 @@
import React from 'react';
import {useQuery, gql} from '@apollo/client';
import CircularProgress from '@mui/material/CircularProgress';
import FormControl from '@mui/material/FormControl';
import FormHelperText from '@mui/material/FormHelperText';
import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select';
import { CreatePayloadNavigationButtons} from './CreatePayloadNavigationButtons';
import Typography from '@mui/material/Typography';
@@ -68,27 +67,27 @@ export function Step1SelectOS(props){
props.canceled();
}
return (
<div >
<Typography variant="h3" align="left" id="selectospage" component="div"
style={{ "marginLeft": "10px"}}>
Select Target Operating System
</Typography> <br/>
<FormControl>
<Select
native
value={os}
onChange={evt => setOS(evt.target.value)}
>
{
osOptions.map((opt) => (
<option key={"step1" + opt} value={opt}>{opt}</option>
))
}
</Select>
<FormHelperText>Target Operating System</FormHelperText>
</FormControl><br/><br/>
<CreatePayloadNavigationButtons first={props.first} last={props.last} canceled={canceled} finished={finished} />
<div style={{height: "100%", display: "flex", flexDirection: "column"}}>
<Typography variant="h3" align="left" id="selectospage" component="div"
style={{"marginLeft": "10px"}}>
Select Target Operating System
</Typography> <br/>
<div style={{flexGrow: 1, width: "100%", display: "flex", flexDirection: "column"}}>
<Select
value={os}
onChange={evt => setOS(evt.target.value)}
>
{
osOptions.map((opt) => (
<MenuItem key={"step1" + opt} value={opt}>{opt}</MenuItem>
))
}
</Select>
</div>
<CreatePayloadNavigationButtons first={props.first} last={props.last} canceled={canceled}
finished={finished}/>
<br/><br/>
</div>
);
}
@@ -7,6 +7,7 @@ import {CreatePayloadBuildParametersTable} from './CreatePayloadBuildParametersT
import {snackActions} from '../../utilities/Snackbar';
import Typography from '@mui/material/Typography';
import {getDefaultValueForType, getDefaultChoices} from '../CreatePayload/Step2SelectPayloadType';
import MenuItem from '@mui/material/MenuItem';
const GET_Payload_Types = gql`
query getPayloadTypesBuildParametersQuery($os: jsonb!) {
@@ -147,24 +148,27 @@ export function Step2SelectPayloadType(props){
return <div>Error! {error.message}</div>;
}
return (
<div >
<Typography variant="h3" align="left" id="selectospage" component="div"
style={{"marginLeft": "10px"}}>
Select Target Payload Type
<div style={{height: "100%", display: "flex", flexDirection: "column"}}>
<Typography variant="h3" align="left" id="selectospage" component="div"
style={{"marginLeft": "10px"}}>
Select Target Payload Type
</Typography>
<Select
native
value={selectedPayloadType}
onChange={changePayloadType}
value={selectedPayloadType}
onChange={changePayloadType}
>
{
data.payloadtype.map((opt) => (
<option key={"step2" + opt.name} value={opt.name}>{opt.name}</option>
))
}
{
data.payloadtype.map((opt) => (
<MenuItem key={"step2" + opt.name} value={opt.name}>{opt.name}</MenuItem>
))
}
</Select><br/>
<CreatePayloadBuildParametersTable onChange={onChange} buildParameters={payloadTypeParameters} />
<CreatePayloadNavigationButtons first={props.first} last={props.last} canceled={canceled} finished={finished} />
<div style={{display: "flex", flexGrow: 1, overflowY: "auto"}}>
<CreatePayloadBuildParametersTable onChange={onChange} buildParameters={payloadTypeParameters}/>
</div>
<CreatePayloadNavigationButtons first={props.first} last={props.last} canceled={canceled}
finished={finished}/>
<br/><br/>
</div>
);
}
@@ -3,13 +3,11 @@ import { styled } from '@mui/material/styles';
import {useQuery, gql} from '@apollo/client';
import { CreatePayloadNavigationButtons} from './CreatePayloadNavigationButtons';
import Typography from '@mui/material/Typography';
import Paper from '@mui/material/Paper';
import Button from '@mui/material/Button';
import { snackActions } from '../../utilities/Snackbar';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import { meState } from '../../../cache';
@@ -17,7 +15,7 @@ import {useReactiveVar} from '@apollo/client';
import {DetailedPayloadTable} from '../Payloads/DetailedPayloadTable';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
import { toLocalTime } from '../../utilities/Time';
import InfoIcon from '@mui/icons-material/Info';
import InfoIconOutline from '@mui/icons-material/InfoOutlined';
import IconButton from '@mui/material/IconButton';
import {b64DecodeUnicode} from '../Callbacks/ResponseDisplay';
@@ -103,25 +101,30 @@ export function Step3SelectPayload(props){
props.canceled();
}
return (
<div >
<Typography variant="h3" align="left" id="selectcommands" component="div"
style={{ "marginLeft": "10px"}}>
Wrap Agent Into New Payload
</Typography> <br/>
<PayloadSelect payloadOptions={payloadOptions} first={props.first} last={props.last}
canceled={canceled} finished={finished}/>
<div style={{height: "100%", display: "flex", flexDirection: "column", width: '100%'}}>
<Typography variant="h3" align="left" id="selectcommands" component="div"
style={{"marginLeft": "10px"}}>
Wrap Agent Into New Payload
</Typography> <br/>
<div style={{flexGrow: 1, overflowY: "auto"}}>
<PayloadSelect payloadOptions={payloadOptions} first={props.first} last={props.last}
canceled={canceled} finished={finished}/>
</div>
<div style={{paddingTop: "20px"}}>
<CreatePayloadNavigationButtons disableNext first={props.first} last={props.last}
canceled={props.canceled} finished={finished}/>
<br/><br/>
</div>
</div>
);
}
function PayloadSelect(props) {
const finished = (payload) => {
props.finished(payload);
}
return (
<Root>
<TableContainer component={Paper}>
<Table stickyHeader size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
const finished = (payload) => {
props.finished(payload);
}
return (
<Table stickyHeader size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "4rem"}}> Select</TableCell>
@@ -142,12 +145,6 @@ return (
))}
</TableBody>
</Table>
</TableContainer>
<div style={{paddingTop: "20px"}}>
<CreatePayloadNavigationButtons disableNext first={props.first} last={props.last} canceled={props.canceled} finished={finished} />
</div>
</Root>
);
}
@@ -167,8 +164,8 @@ export function PayloadsTableRow(props){
<TableCell>{b64DecodeUnicode(props.payload.filemetum.filename_text)}</TableCell>
<TableCell>{props.payload.description}</TableCell>
<TableCell>
<IconButton size="small" color="primary" onClick={() => setOpenDetailedView(true)}>
<InfoIcon />
<IconButton size="small" color="info" onClick={() => setOpenDetailedView(true)}>
<InfoIconOutline />
</IconButton>
</TableCell>
</TableRow>
@@ -24,6 +24,7 @@ export function Step5Build(props){
const [description, setDescription] = React.useState("");
const [startSubscription, setStartSubscription] = React.useState(false);
const [subscriptionID, setSubscriptionID] = React.useState("");
const [showExtraOptions, setShowExtraOptions] = React.useState(false);
const [createPayloadMutation] = useMutation(create_payload, {
update: (cache, {data}) => {
if(data.createPayload.status === "success"){
@@ -31,6 +32,7 @@ export function Step5Build(props){
if(!startSubscription){
setStartSubscription(true);
}
setShowExtraOptions(true);
snackActions.info("Submitted payload to build pipeline", {autoClose: 1000});
}else{
snackActions.error(data.createPayload.error);
@@ -64,17 +66,37 @@ export function Step5Build(props){
}, {});
buildParameters.push({name: param.name, value: newDict});
} else if (param.parameter_type === "File") {
if(typeof param.value === "string"){
if (typeof param.value === "string") {
buildParameters.push({name: param.name, value: param.value});
} else {
const newUUID = await UploadTaskFile(param.value, "Uploaded as build parameter for " + filename);
if (newUUID) {
buildParameters.push({name: param.name, value: newUUID});
if (newUUID !== "Missing file in form") {
buildParameters.push({name: param.name, value: newUUID});
}
} else {
snackActions.error("Failed to upload files")
return;
}
}
} else if(param.parameter_type === "FileMultiple") {
let fileMultipleValues = [];
for(let j = 0; j < param.value.length; j++){
if (typeof param.value[j] === "string") {
fileMultipleValues.push(param.value[j]);
} else {
const newUUID = await UploadTaskFile(param.value[j], "Uploaded as build parameter for " + filename);
if (newUUID) {
if (newUUID !== "Missing file in form") {
fileMultipleValues.push(newUUID);
}
} else {
snackActions.error("Failed to upload files")
return;
}
}
}
buildParameters.push({name: param.name, value: fileMultipleValues});
} else {
buildParameters.push({name: param.name, value: param.value});
}
@@ -97,15 +119,19 @@ export function Step5Build(props){
}
return (
<div >
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%"}}>
<Typography variant="h3" align="left" id="selectc2profiles" component="div"
style={{"marginLeft": "10px"}}>
Payload Review
</Typography>
<br/>
<MythicTextField onEnter={finished} autoFocus={true} required={false} placeholder={"Filename"} value={filename} multiline={false} onChange={onChangeFilename} display="inline-block"/>
<MythicTextField onEnter={finished} required={false} placeholder={"description"} value={description} multiline={false} onChange={onChangeDescription} display="inline-block"/>
<CreatePayloadNavigationButtons first={props.first} last={props.last} canceled={canceled} finished={finished} startOver={props.startOver} />
<div style={{display: "flex", flexDirection: "column", flexGrow: 1}}>
<MythicTextField onEnter={finished} autoFocus={true} required={false} placeholder={"Filename"} value={filename} multiline={false} onChange={onChangeFilename} display="inline-block"/>
<MythicTextField onEnter={finished} required={false} placeholder={"description"} value={description} multiline={false} onChange={onChangeDescription} display="inline-block"/>
</div>
<CreatePayloadNavigationButtons first={props.first} last={props.last} canceled={canceled}
finished={finished} startOver={props.startOver} showExtraOptions={showExtraOptions} />
<br/><br/>
{startSubscription && <PayloadSubscriptionNotification me={props.me} subscriptionID={subscriptionID} fromNow={fromNow}/>}
</div>
@@ -47,7 +47,7 @@ export function EventFeedTable(props){
return (
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%"}}>
<div>
<Grid container spacing={2} style={{paddingTop: "10px", maxWidth: "100%"}}>
<Grid container spacing={2} style={{maxWidth: "100%"}}>
<Grid item xs={10}>
<MythicTextField placeholder="Search..." value={search}
onChange={handleSearchValueChange} onEnter={submitSearch} name="Search..." InputProps={{
@@ -66,8 +66,8 @@ export function EventFeedTableEvents(props){
const me = useReactiveVar(meState);
return (
<StyledListItem alignItems="flex-start" style={{...props.style, margin: 0, padding: "0 0 0 10px"}}>
<ListItemText disableTypography style={{margin: "0 0 0 0"}}
<StyledListItem alignItems="flex-start" style={{...props.style, margin: 0, width: "100%", padding: "0 0 0 10px"}}>
<ListItemText disableTypography style={{margin: "0 0 0 0", flexGrow: 1, overflowX: 'auto'}}
primary={
<React.Fragment>
<Typography
@@ -90,16 +90,16 @@ export function EventFeedTableEvents(props){
</React.Fragment>
}
secondary={
<div >
<div style={{display: "flex", overflowX: "auto"}}>
<GetPreAdornment message={props} />
<pre style={{overflowX: "auto", display: "inline-flex", maxWidth: "97%", margin: "0 0 0 0px"}}>
<pre style={{ margin: "0 0 0 0px"}}>
{props.message}
</pre>
</div>
}
/>
<EventFeedTableEventsActions id={props.id} level={props.level}
onUpdateResolution={props.onUpdateResolution}
onUpdateResolution={props.onUpdateResolution}
onUpdateLevel={props.onUpdateLevel}
resolved={props.resolved}/>
</StyledListItem>
@@ -23,12 +23,13 @@ export function EventFeedTableEventsActions(props){
setAnchorEl(e.currentTarget)
}, []);
return (
<ListItemSecondaryAction>
<ListItemSecondaryAction >
<IconButton
aria-controls={"eventmenu" + props.id}
aria-haspopup="true"
onClick={handleClick}
size="large"><MoreVertIcon/></IconButton>
size="large"><MoreVertIcon/>
</IconButton>
<Menu elevation={5} id={"eventmenu" + props.id} anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleClose} >
{
props.level === "warning" && props.resolved ? (
@@ -0,0 +1,139 @@
import React from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import {useMutation, gql} from '@apollo/client';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import {toLocalTime} from "../../utilities/Time";
import CheckCircleTwoToneIcon from '@mui/icons-material/CheckCircleTwoTone';
import CancelTwoToneIcon from '@mui/icons-material/CancelTwoTone';
import {snackActions} from "../../utilities/Snackbar";
const updateApprovalStatusMutation = gql`
mutation updateApprovalStatus($eventgroupapproval_id: Int!, $approved: Boolean!) {
updateEventGroupApproval(eventgroupapproval_id: $eventgroupapproval_id, approved: $approved) {
status
error
}
}
`;
export function EventGroupTableRunAsDialog({eventgroupapprovals, me, onClose, selectedEventGroup}) {
const [UpdateApprovalStatusMutation] = useMutation(updateApprovalStatusMutation, {
onCompleted: (data) => {
if(data.updateEventGroupApproval.status === "success"){
snackActions.success("Updated approval");
} else {
snackActions.error(data.updateEventGroupApproval.error);
}
},
onError: (data) => {
console.log(data);
snackActions.error("Failed to update");
}
});
const onApprovalClick = ({id, approved}) => {
UpdateApprovalStatusMutation({variables: {eventgroupapproval_id: id, approved}})
}
const getRunAsHelp = (run_as) => {
switch(run_as){
case "bot":
return "These actions run under a 'bot' account rather than as a normal operator account.";
case "self":
return "These actions run under the context of the operator that created the workflow";
case "trigger":
return "These actions run under the context of the operator that triggered the workflow";
default:
return "These actions run under the context of the identified operator";
}
}
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Approve or Deny Event Workflow Execution Per Operator</DialogTitle>
<DialogContent dividers={true} style={{maxHeight: "calc(70vh)"}}>
<DialogContentText>
Individual users must approve workflows to run under their account and operation leads must approve bot workflows.<br/>
<b>Run As: </b>{selectedEventGroup.run_as} <br/>
{getRunAsHelp(selectedEventGroup.run_as)}
</DialogContentText>
<Table>
<TableHead>
<TableRow>
<TableCell>Operator</TableCell>
<TableCell>Approval Status</TableCell>
<TableCell>Last Updated</TableCell>
</TableRow>
</TableHead>
<TableBody>
{eventgroupapprovals.map( e => (
<TableRow key={e.id}>
<TableCell>{e.operator.username}</TableCell>
<TableCell>
{e.approved ? (
<>
<Button disabled style={{marginRight: "10px"}} variant={"contained"} color={"success"}
onClick={() => onApprovalClick({id: e.id, approved: true})}>
<CheckCircleTwoToneIcon color={"success"} style={{marginRight: "5px"}} /> Approved
</Button>
<Button disabled={e.operator.id !== me?.user?.id} variant={"contained"} color={"warning"}
onClick={() => onApprovalClick({id: e.id, approved: false})}>
<CancelTwoToneIcon style={{marginRight: "5px"}} /> Deny
</Button>
</>
) : e.created_at === e.updated_at ? (
<>
<Button disabled={e.operator.id !== me?.user?.id} style={{marginRight: "10px"}}
variant={"contained"} color={"success"}
onClick={() => onApprovalClick({id: e.id, approved: true})}>
<CheckCircleTwoToneIcon style={{marginRight: "5px"}} /> Approve
</Button>
<Button disabled={e.operator.id !== me?.user?.id} variant={"contained"}
color={"warning"}
onClick={() => onApprovalClick({id: e.id, approved: false})}>
<CancelTwoToneIcon style={{marginRight: "5px"}} /> Deny
</Button>
</>
) : (
<>
<Button style={{marginRight: "10px"}} variant={"contained"} color={"success"}
disabled={e.operator.id !== me?.user?.id}
onClick={() => onApprovalClick({id: e.id, approved: true})}>
<CheckCircleTwoToneIcon style={{marginRight: "5px"}} /> Approve
</Button>
<Button disabled variant={"contained"} color={"warning"}
onClick={() => onApprovalClick({id: e.id, approved: false})}>
<CancelTwoToneIcon color={"warning"} style={{marginRight: "5px"}} /> Denied
</Button>
</>
)}
</TableCell>
<TableCell>
{e.created_at !== e.updated_at ? (
toLocalTime(e?.updated_at, me?.user?.view_utc_time)
) : null}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
Close
</Button>
</DialogActions>
</React.Fragment>
);
}
@@ -0,0 +1,205 @@
import React from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import {useMutation, gql} from '@apollo/client';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import {snackActions} from "../../utilities/Snackbar";
import {UploadEventGroupFile} from "../../MythicComponents/MythicFileUpload";
import {b64DecodeUnicode} from "../Callbacks/ResponseDisplay";
import {previewFileQuery, updateFileDeleted} from "../Search/FileMetaTable";
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import hexFile from "../../../assets/file_bin.png";
import txtFile from "../../../assets/file_txt.png";
import {MythicDialog} from "../../MythicComponents/MythicDialog";
import {PreviewFileMediaDialog} from "../Search/PreviewFileMedia";
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
import {PreviewFileStringDialog} from "../Search/PreviewFileStringDialog";
import {PreviewFileHexDialog} from "../Search/PreviewFileHexDialog";
import {faPhotoVideo} from '@fortawesome/free-solid-svg-icons';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import DeleteIcon from '@mui/icons-material/Delete';
import {Link, Typography} from '@mui/material';
import {MythicConfirmDialog} from "../../MythicComponents/MythicConfirmDialog";
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
export function EventFileManageDialog({onClose, selectedEventGroup}) {
const onFileChange = async (evt) => {
for(let i = 0; i < evt.target.files.length; i++){
let uploadStatus = await UploadEventGroupFile(evt.target.files[i], selectedEventGroup.id);
if(uploadStatus.status === "error"){
snackActions.error(uploadStatus.error);
}
}
}
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">
Add or Remove Files associated with this workflow
<Button size={"small"} style={{float: "right", marginRight: "10px"}}
variant={"contained"}
color={"success"} component="label"
startIcon={<CloudUploadIcon />}
>
New Files
<input onChange={onFileChange} type="file" multiple hidden/>
</Button>
</DialogTitle>
<DialogContent dividers={true} style={{maxHeight: "calc(70vh)"}}>
<DialogContentText>
These files can be accessed via `eventgroup.filename` in a step's inputs.
</DialogContentText>
<Table>
<TableBody>
{selectedEventGroup.filemeta.map( e => (
<EventFileManageDialogTableRow key={e.id} eventFile={e} />
))}
</TableBody>
</Table>
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
Close
</Button>
</DialogActions>
</React.Fragment>
);
}
function EventFileManageDialogTableRow({eventFile}) {
const [openDelete, setOpenDelete] = React.useState(false);
const [openPreviewStringsDialog, setOpenPreviewStringsDialog] = React.useState(false);
const [openPreviewHexDialog, setOpenPreviewHexDialog] = React.useState(false);
const [openPreviewMediaDialog, setOpenPreviewMediaDialog] = React.useState(false);
const [deleteFile] = useMutation(updateFileDeleted, {
onCompleted: (data) => {
snackActions.dismiss();
},
onError: (data) => {
console.log(data);
snackActions.error("Failed to delete file");
}
});
const [previewFileString] = useMutation(previewFileQuery, {
onCompleted: (data) => {
if(data.previewFile.status === "success"){
setFileContents(data.previewFile.contents);
setOpenPreviewStringsDialog(true);
}else{
snackActions.error(data.previewFile.error)
}
},
onError: (data) => {
console.log(data);
snackActions.error(data);
}
});
const [previewFileHex] = useMutation(previewFileQuery, {
onCompleted: (data) => {
if(data.previewFile.status === "success"){
setFileContents(data.previewFile.contents);
setOpenPreviewHexDialog(true);
}else{
snackActions.error(data.previewFile.error)
}
},
onError: (data) => {
console.log(data);
snackActions.error(data);
}
});
const onAcceptDelete = () => {
deleteFile({variables: {file_id: eventFile.id}})
}
const [fileContents, setFileContents] = React.useState('');
const onPreviewStrings = (event) => {
if(event){
event.preventDefault();
event.stopPropagation();
}
previewFileString({variables: {file_id: eventFile.agent_file_id}})
}
const onPreviewHex = (event) => {
if(event){
event.preventDefault();
event.stopPropagation();
}
previewFileHex({variables: {file_id: eventFile.agent_file_id}})
}
const onPreviewMedia = (event) => {
if(event){
event.preventDefault();
event.stopPropagation();
}
setOpenPreviewMediaDialog(true);
}
return (
<TableRow >
<MythicStyledTableCell style={{display: "flex", alignItems: "baseline"}}>
{eventFile.deleted ? null : (
<>
{openDelete &&
<MythicConfirmDialog onClose={() => {setOpenDelete(false);}} onSubmit={onAcceptDelete} open={openDelete}/>
}
<DeleteIcon color={"error"} fontSize={"large"} style={{height: "35px", cursor: "pointer"}}
onClick={()=>{setOpenDelete(true);}}/>
<MythicStyledTooltip title={"Preview HEX XXD"}>
<img src={hexFile} alt={"preview hex"} style={{height: "35px", cursor: "pointer"}}
onClick={onPreviewHex}/>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Preview Strings"}>
<img src={txtFile} alt={"preview strings"} style={{height: "35px", cursor: "pointer"}}
onClick={onPreviewStrings} />
</MythicStyledTooltip>
<MythicStyledTooltip title={"Preview Media"}>
<FontAwesomeIcon icon={faPhotoVideo} style={{height: "25px", bottom: "5px", position: "relative", cursor: "pointer", display: "inline-block"}}
onClick={onPreviewMedia} />
</MythicStyledTooltip>
{openPreviewMediaDialog &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openPreviewMediaDialog}
onClose={(e)=>{setOpenPreviewMediaDialog(false);}}
innerDialog={<PreviewFileMediaDialog
agent_file_id={eventFile.agent_file_id}
filename={b64DecodeUnicode(eventFile.filename_text)}
onClose={(e)=>{setOpenPreviewMediaDialog(false);}} />}
/>
}
{openPreviewStringsDialog &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openPreviewStringsDialog}
onClose={()=>{setOpenPreviewStringsDialog(false);}}
innerDialog={<PreviewFileStringDialog onClose={()=>{setOpenPreviewStringsDialog(false);}}
filename={b64DecodeUnicode(eventFile.filename_text)} contents={fileContents}
/>}
/>
}
{openPreviewHexDialog &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openPreviewHexDialog}
onClose={()=>{setOpenPreviewHexDialog(false);}}
innerDialog={<PreviewFileHexDialog onClose={()=>{setOpenPreviewHexDialog(false);}}
filename={b64DecodeUnicode(eventFile.filename_text)} contents={fileContents}
/>}
/>
}
</>
)}
</MythicStyledTableCell>
<MythicStyledTableCell style={{}}>
{eventFile.deleted ? (
<Typography variant="body2" style={{wordBreak: "break-all"}}>{b64DecodeUnicode(eventFile.filename_text)}</Typography>
) : (
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" href={"/direct/download/" + eventFile.agent_file_id}>{b64DecodeUnicode(eventFile.filename_text)}</Link>
)
}
</MythicStyledTableCell>
</TableRow>
)
}
@@ -0,0 +1,87 @@
import React from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
import {Typography} from '@mui/material';
import {useTheme} from '@mui/material/styles';
export function EventGroupConsumingContainersDialog({onClose, selectedEventGroup}) {
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">
Eventing containers needed for this workflow to succeed
</DialogTitle>
<DialogContent dividers={true} style={{maxHeight: "calc(70vh)"}}>
<Table>
<TableHead>
<TableRow>
<TableCell>Container Name</TableCell>
<TableCell>Container Status</TableCell>
<TableCell>Needed Functions</TableCell>
<TableCell>Available Functions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{selectedEventGroup.eventgroupconsumingcontainers.map( e => (
<EventGroupConsumingContainersDialogTableRow key={e.id} container={e} />
))}
</TableBody>
</Table>
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
Close
</Button>
</DialogActions>
</React.Fragment>
);
}
function EventGroupConsumingContainersDialogTableRow({container}) {
const theme = useTheme();
const [subscriptions, setSubscriptions] = React.useState([]);
React.useEffect( () => {
if(container.consuming_container){
const parsedLocalEventing = container?.consuming_container?.subscriptions?.map( s => {
try{
return JSON.parse(s);
}catch(error){
console.log(error);
return {name: "", description: s};
}
})
setSubscriptions(parsedLocalEventing);
}
}, [container?.consuming_container?.subscriptions])
return (
<TableRow >
<MythicStyledTableCell >{container.consuming_container_name}</MythicStyledTableCell>
<MythicStyledTableCell>
<Typography variant="body2" component="p" color={container?.consuming_container?.container_running ? theme.palette.success.main : theme.palette.error.main} >
<b>{container?.consuming_container ? container?.consuming_container?.container_running ? "Online" : "Offline" : "Doesn't Exist"}</b>
</Typography>
</MythicStyledTableCell>
<MythicStyledTableCell>
{container.function_names.join(", ")}
</MythicStyledTableCell>
<MythicStyledTableCell>
{subscriptions.map(s => (
<Typography key={s.name}>
<b>{s.name}</b> - {s.description}
</Typography>
))}
</MythicStyledTableCell>
</TableRow>
)
}
@@ -0,0 +1,105 @@
import React, {} from 'react';
import { gql, useSubscription, useQuery } from '@apollo/client';
import {EventGroupInstancesTableMaterialReactTable} from "./EventGroupInstancesTable";
const get_eventgroups = gql`
query GetEventGroupInstances {
eventgroupinstance(limit: 100, order_by: {id: desc}) {
id
created_at
end_timestamp
trigger
operator {
username
}
status
updated_at
eventgroup {
name
description
deleted
active
id
}
}
}
`;
const sub_eventgroups = gql`
subscription GetEventGroupInstances {
eventgroupinstance_stream(cursor: {initial_value: {updated_at: "1970-01-01"}, ordering: ASC}, batch_size: 100, where: {eventgroup: {}}) {
id
created_at
end_timestamp
trigger
operator {
username
}
status
updated_at
eventgroup {
name
description
deleted
active
id
}
}
}
`;
export function EventGroupInstances({selectedEventGroup, me, setSelectedInstance, selectedInstanceID}) {
const [eventgroups, setEventgroups] = React.useState([]);
const [selectedEventGroups, setSelectedEventGroups] = React.useState([]);
React.useEffect(() => {
if(selectedEventGroup.id === 0){
setSelectedEventGroups(eventgroups);
return;
}
const updatedEventGroups = eventgroups.filter(e => e.eventgroup.id === selectedEventGroup.id);
setSelectedEventGroups(updatedEventGroups);
}, [selectedEventGroup.id, eventgroups]);
useQuery(get_eventgroups, {
fetchPolicy: "no-cache",
onCompleted: (data) => {
setEventgroups((prevState) => {
const newEvents = data.eventgroupinstance.reduce( (prev, cur) => {
let indx = prev.findIndex( ({id}) => id === cur.id);
if(indx > -1){
let updatingPrev = [...prev];
updatingPrev[indx] = cur;
return [...updatingPrev];
}
return [...prev, cur];
}, [...prevState]);
newEvents.sort((a,b) => (a.id > b.id) ? -1 : ((b.id > a.id) ? 1 : 0));
return newEvents;
});
},
onError: (data) => {
console.log(data);
}
})
useSubscription(sub_eventgroups, {
fetchPolicy: "no-cache",
onData: ({data}) => {
setEventgroups((prevState) => {
const newEvents = data.data.eventgroupinstance_stream.reduce( (prev, cur) => {
let indx = prev.findIndex( ({id}) => id === cur.id);
if(indx > -1){
let updatingPrev = [...prev];
updatingPrev[indx] = cur;
return [...updatingPrev];
}
return [...prev, cur];
}, [...prevState]);
newEvents.sort((a,b) => (a.id > b.id) ? -1 : ((b.id > a.id) ? 1 : 0));
return newEvents;
});
}
});
return (
<EventGroupInstancesTableMaterialReactTable setSelectedInstance={setSelectedInstance}
selectedInstanceID={selectedInstanceID}
eventgroups={selectedEventGroups} me={me} />
)
}
@@ -0,0 +1,446 @@
import React, {} from 'react';
import {EventStepInstanceRenderDialog, GetStatusSymbol} from "./EventStepRender";
import {toLocalTime} from "../../utilities/Time";
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {MythicConfirmDialog} from '../../MythicComponents/MythicConfirmDialog';
import { gql, useMutation } from '@apollo/client';
import CalendarMonthTwoToneIcon from '@mui/icons-material/CalendarMonthTwoTone';
import AccessAlarmTwoToneIcon from '@mui/icons-material/AccessAlarmTwoTone';
import CancelTwoToneIcon from '@mui/icons-material/CancelTwoTone';
import ReplayIcon from '@mui/icons-material/Replay';
import IconButton from '@mui/material/IconButton';
import Moment from 'react-moment';
import moment from 'moment';
import CastConnectedTwoToneIcon from '@mui/icons-material/CastConnectedTwoTone';
import {snackActions} from "../../utilities/Snackbar";
import {MythicDialog} from "../../MythicComponents/MythicDialog";
import OpenInNewTwoToneIcon from '@mui/icons-material/OpenInNewTwoTone';
import Typography from '@mui/material/Typography';
import IosShareIcon from '@mui/icons-material/IosShare';
import {copyStringToClipboard} from "../../utilities/Clipboard";
import {useTheme} from '@mui/material/styles';
import {
MaterialReactTable,
useMaterialReactTable,
} from 'material-react-table';
const cancelEventGroupInstanceMutation = gql(`
mutation cancelEventGroupInstanceMutation($eventgroupinstance_id: Int!){
eventingTriggerCancel(eventgroupinstance_id: $eventgroupinstance_id){
status
error
}
}
`);
const retryEventGroupInstanceMutation = gql(`
mutation retryEventGroupInstanceMutation($eventgroupinstance_id: Int!){
eventingTriggerRetry(eventgroupinstance_id: $eventgroupinstance_id){
status
error
}
}
`);
const runAgainEventGroupInstanceMutation = gql(`
mutation runAgainEventGroupInstanceMutation($eventgroupinstance_id: Int!){
eventingTriggerRunAgain(eventgroupinstance_id: $eventgroupinstance_id){
status
error
}
}
`);
export const adjustDurationOutput = (e, newTime) => {
let start = moment(e.created_at);
let end = moment(e.end_timestamp !== null ? e.end_timestamp : e.updated_at);
let diffDuration = moment.duration(end.diff(start));
let output = "";
if(diffDuration.days() > 0){
output += diffDuration.days() + "d ";
}
if(diffDuration.hours() > 0){
output += diffDuration.hours() + "h ";
}
if(diffDuration.minutes() > 0){
output += diffDuration.minutes() + "m ";
}
if(diffDuration.seconds() > 0){
output += diffDuration.seconds() + "s ";
}
if(output === ""){
return "1s";
}
return output;
}
export const adjustOutput = (e, newTime) => {
if(newTime === "a few seconds"){
moment.relativeTimeThreshold('s', 60);
moment.relativeTimeThreshold('ss', 0);
return moment(e.created_at + "Z", "YYYY-MM-DDTHH:mm:ss.SSSSSSZ").fromNow(true)
}
return newTime;
}
const accessorFn = (row, h) => {
if(h.type === "timestamp"){
let d = new Date(row[h.key] || 0);
if (d.getFullYear() === 1970){
d = new Date();
d = d + d.getTimezoneOffset();
}
return d;
}
if(h.type === "number" || h.type === "size"){
try{
return Number(row[h.key] || 0);
}catch(error){
return row[h.key] || 0;
}
}
return row[h.key] || "";
};
function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, setSelectedInstance, selectedInstanceID}){
const theme = useTheme();
const [openDeleteDialog, setOpenDeleteDialog] = React.useState(false);
const [openRetryDialog, setOpenRetryDialog] = React.useState(false);
const [openRunAgainDialog, setOpenRunAgainDialog] = React.useState(false);
const [openEventStepRender, setOpenEventStepRender] = React.useState(false);
const selectedLocalInstanceID = React.useRef(0);
const selectedLocalEventGroup = React.useRef({});
const foundQueryEvent = React.useRef(false);
const [CancelEventGroupInstance] = useMutation(cancelEventGroupInstanceMutation, {
onCompleted: (data) => {
if(data.eventingTriggerCancel.status === "success"){
snackActions.info("Successfully sent cancel request");
} else {
snackActions.error(data.eventingTriggerCancel.error);
}
},
onError: (data) => {
}
})
const [RetryEventGroupInstance] = useMutation(retryEventGroupInstanceMutation, {
onCompleted: (data) => {
if(data.eventingTriggerRetry.status === "success"){
snackActions.info("Successfully sent retry request");
} else {
snackActions.error(data.eventingTriggerRetry.error);
}
},
onError: (data) => {
}
});
const [RunAgainEventGroupInstance] = useMutation(runAgainEventGroupInstanceMutation, {
onCompleted: (data) => {
if(data.eventingTriggerRunAgain.status === "success"){
snackActions.info("Successfully sent run again request");
} else {
snackActions.error(data.eventingTriggerRunAgain.error);
}
},
onError: (data) => {
}
});
const onCancelSubmit = () => {
CancelEventGroupInstance({variables: {eventgroupinstance_id: selectedLocalInstanceID.current}})
}
const onRetrySubmit = () => {
RetryEventGroupInstance({variables: {eventgroupinstance_id: selectedLocalInstanceID.current}});
}
const onRunAgainSubmit = () => {
RunAgainEventGroupInstance({variables: {eventgroupinstance_id: selectedLocalInstanceID.current}});
}
const onOpenCancelDialog = ({id}) => {
selectedLocalInstanceID.current = id;
setOpenDeleteDialog(true);
}
const onOpenRetryDialog = ({id}) => {
selectedLocalInstanceID.current = id;
setOpenRetryDialog(true);
}
const onOpenRunAgainDialog = ({id}) => {
selectedLocalInstanceID.current = id;
setOpenRunAgainDialog(true);
}
const openViewInstanceLargeDialog = (e) => {
selectedLocalInstanceID.current = e.id;
selectedLocalEventGroup.current = e.eventgroup;
setOpenEventStepRender(true);
}
const onSaveToClipboard = (e) => {
let path = window.location.origin;
path += `/new/eventing?eventgroup=${e.eventgroup.id}&eventgroupinstance=${e.id}`;
copyStringToClipboard(path);
snackActions.success("copied shareable link to clipboard");
}
React.useEffect( () => {
if( !foundQueryEvent.current ){
let queryParams = new URLSearchParams(window.location.search);
const eventgroup = queryParams.has("eventgroupinstance") ? queryParams.get("eventgroupinstance") : "0";
if(eventgroup !== "0"){
let matchedGroup = eventgroups.find( e => `${e.id}` === eventgroup);
if(matchedGroup){
setSelectedInstance(matchedGroup.id);
foundQueryEvent.current = true;
}
}
}
}, [eventgroups]);
const columnFields = [
{key: "id", type: "number", name: "ID", width: 40, disableCopy: true, enableHiding: false, disableSort: true, disableFilter: true},
{key: "status", type: 'string', name: "Status", width: 100, disableCopy: true, disableSort: true, enableHiding: false},
{key: "event_group", type: 'string', name: "Event Group", fillWidth: true, disableCopy: true, enableHiding: false},
{key: "trigger", type: 'string', name: "Trigger", fillWidth: true, enableHiding: false},
{key: "time", type: 'date', name: "Time", width: 180, enableHiding: false},
{key: "operator", type: 'string', name: "Operator", fillWidth: true, enableHiding: false},
{key: "cancel", type: 'string', name: "Action", width: 50, disableCopy: true, enableHiding: false, disableSort: true, disableFilter: true},
];
const localCellRender = React.useCallback( ({cell, h}) => {
let row = cell.row?.original;
switch(h.name){
case "ID":
return (
row.id
)
case "Status":
return (
<>
<GetStatusSymbol data={row} />
{
selectedInstanceID === 0 ?
(
<IconButton onClick={() => {setSelectedInstance(row.id);}} >
<CastConnectedTwoToneIcon />
</IconButton>
) :
(
<IconButton onClick={() => {setSelectedInstance(0);}} >
<CancelTwoToneIcon />
</IconButton>
)
}
<IconButton onClick={() => {openViewInstanceLargeDialog(row)}}>
<OpenInNewTwoToneIcon />
</IconButton>
<IconButton onClick={() => onSaveToClipboard(row)}>
<IosShareIcon />
</IconButton>
</>
)
case "Event Group":
return (
<Typography >
{row.eventgroup.name}
</Typography>
)
case "Trigger":
return (
row.trigger
)
case "Time":
return (
<div>
<div style={{display: "flex", flexDirection: "row", alignItems: "center"}}>
<CalendarMonthTwoToneIcon style={{marginRight: "10px"}}/>
{toLocalTime(row?.created_at, me?.user?.view_utc_time)}
</div>
<div style={{display: "flex", flexDirection: "row", alignItems: "center"}}>
<AccessAlarmTwoToneIcon style={{marginRight: "10px"}}/>
{row.end_timestamp === null &&
<Moment filter={(newTime) => adjustOutput(row, newTime)} interval={1000}
parse={"YYYY-MM-DDTHH:mm:ss.SSSSSSZ"}
withTitle
titleFormat={"YYYY-MM-DD HH:mm:ss"}
fromNow ago
>
{row.created_at + "Z"}
</Moment>
}
{row.end_timestamp !== null &&
<Moment filter={(newTime) => adjustDurationOutput(row, newTime)}
parse={"YYYY-MM-DDTHH:mm:ss.SSSSSSZ"}
withTitle
titleFormat={"YYYY-MM-DD HH:mm:ss"}
>
{row.created_at + "Z"}
</Moment>
}
</div>
</div>
)
case "Operator":
return (
row?.operator?.username
)
case "Action":
return (
row.end_timestamp === null ? (
<MythicStyledTooltip title={"Cancel Eventing"} >
<IconButton onClick={() => {onOpenCancelDialog({id: row.id});}} color={"warning"}>
<CancelTwoToneIcon />
</IconButton>
</MythicStyledTooltip>
) : row.status === "error" || row.status === "cancelled" ? (
<MythicStyledTooltip title={"Retry Failed / Canceled Steps"} >
<IconButton onClick={() => {onOpenRetryDialog({id: row.id});}} color={"warning"}>
<ReplayIcon />
</IconButton>
</MythicStyledTooltip>
) : row.status === "success" ? (
<MythicStyledTooltip title={"Run Again"} >
<IconButton onClick={() => {onOpenRunAgainDialog({id: row.id});}} color={"success"}>
<ReplayIcon />
</IconButton>
</MythicStyledTooltip>
) : null
)
}
}, [selectedInstanceID]);
const columns = React.useMemo(() => columnFields.map(h => {
return {
accessorKey: h.key,
header: h.name,
size: h.width,
id: h.key,
enableClickToCopy: !h.disableCopy,
filterVariant: h.type === 'number' || h.type === 'size' ? 'range' : 'text',
enableResizing: true,
enableHiding: h.enableHiding,
enableSorting: !h.disableSort,
enableColumnFilter: !h.disableFilter,
grow: h.fillWidth,
accessorFn: (row) => accessorFn(row, h),
Cell: ({cell}) => localCellRender({cell, h})
}
}), [columnFields])
const materialReactTable = useMaterialReactTable({
columns,
data: eventgroups,
layoutMode: "grid",
autoResetPageIndex: false,
enableFacetedValues: true,
enablePagination: true,
//enableRowVirtualization: true,
enableBottomToolbar: false,
enableStickyHeader: true,
enableDensityToggle: false,
enableColumnResizing: true,
enableRowPinning: false,
positionPagination: "top",
columnFilterDisplayMode: 'popover', //filter inputs will show in a popover (like excel)
rowPinningDisplayMode: 'top-and-bottom',
//enableColumnOrdering: true,
//columnResizeMode: 'onEnd',
initialState: {
density: 'compact',
},
defaultDisplayColumn: { enableResizing: true },
muiTableContainerProps: { sx: { alignItems: "flex-start" } },
mrtTheme: (theme) => ({
baseBackgroundColor: theme.palette.background.default, //change default background color
}),
muiSearchTextFieldProps: {
placeholder: 'Search loaded data',
size: 'small',
sx: { minWidth: '300px' },
variant: 'outlined',
},
muiTableHeadCellProps: {
sx: {
border: '1px solid rgba(81, 81, 81, .5)',
fontStyle: 'italic',
fontWeight: 'bold',
},
style: {
zIndex: 1,
height: "36px",
}
},
muiTableHeadRowProps: {
sx: {
alignItems: "flex-start",
height: "36px",
}
},
muiTableBodyCellProps: ({ cell, table }) => {
return {
sx: {
padding: "0 0 0 10px",
}
}
},
muiTableBodyRowProps: ({ row }) => ({
sx: {
height: "40px",
},
style: {padding: 0}
}),
enableRowActions: false,
muiTablePaperProps: {
sx: { display: "flex", flexDirection: "column", width: "100%"}
},
muiTopToolbarProps: {
sx: {
backgroundColor: theme.materialReactTableHeader,
display: "flex",
justifyContent: "flex-start"
}
},
renderEmptyRowsFallback: ({ table }) => (
<div style={{display: "flex", width: "100%", height: "100%", justifyContent: "center", flexDirection: "column", alignItems: "center"}}>
<Typography variant={"h4"} >
{eventgroups.length === 0 ? "No Workflows" : null}
</Typography>
</div>
),
});
return (
<div style={{ width: '100%', overflow: "auto", flexGrow: 1, display: "flex"}}>
<MaterialReactTable table={materialReactTable} />
{openDeleteDialog &&
<MythicConfirmDialog onClose={() => {
setOpenDeleteDialog(false);
}} onSubmit={onCancelSubmit} open={openDeleteDialog}
acceptText={"Cancel Workflow"} cancelText={"Keep Running"}
dialogText={"Cancel this workflow and all remaining steps?"}
/>
}
{openRetryDialog &&
<MythicConfirmDialog onClose={() => {
setOpenRetryDialog(false);
}} onSubmit={onRetrySubmit} open={openRetryDialog}
acceptText={"Retry"} cancelText={"Cancel"}
acceptColor={"success"}
dialogText={"Retry all failed / canceled steps?"}
/>
}
{openRunAgainDialog &&
<MythicConfirmDialog onClose={() => {
setOpenRunAgainDialog(false);
}} onSubmit={onRunAgainSubmit} open={openRunAgainDialog}
acceptText={"Run Again"} cancelText={"Cancel"}
acceptColor={"success"}
dialogText={"Run this entire event group again?"}
/>
}
{openEventStepRender &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openEventStepRender}
onClose={() => {
setOpenEventStepRender(false);
}}
innerDialog={
<EventStepInstanceRenderDialog onClose={() => { setOpenEventStepRender(false); }}
selectedEventGroupInstance={selectedLocalInstanceID.current}
selectedEventGroup={selectedLocalEventGroup.current}
/>}
/>
}
</div>
)
}
export const EventGroupInstancesTableMaterialReactTable = React.memo(EventGroupInstancesTableMaterialReactTablePreMemo)
@@ -0,0 +1,463 @@
import React, {useCallback} from 'react';
import { gql, useMutation } from '@apollo/client';
import {snackActions} from '../../utilities/Snackbar';
import {useTheme} from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import {toLocalTime} from "../../utilities/Time";
import NotificationsActiveTwoToneIcon from '@mui/icons-material/NotificationsActiveTwoTone';
import NotificationsOffTwoToneIcon from '@mui/icons-material/NotificationsOffTwoTone';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import InfoTwoToneIcon from '@mui/icons-material/InfoTwoTone';
import {MythicDialog, MythicViewJSONAsTableDialog} from '../../MythicComponents/MythicDialog';
import {
EventStepInstanceRenderFlowWithProvider,
EventStepRenderDialog,
EventStepRenderFlowWithProvider
} from "./EventStepRender";
import OpenInNewTwoToneIcon from '@mui/icons-material/OpenInNewTwoTone';
import PermMediaTwoToneIcon from '@mui/icons-material/PermMediaTwoTone';
import {PreviewFileMediaDialog} from "../Search/PreviewFileMedia";
import {b64DecodeUnicode} from "../Callbacks/ResponseDisplay";
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import IconButton from '@mui/material/IconButton';
import MythicTableCell from "../../MythicComponents/MythicTableCell";
import {EventGroupInstances} from "./EventGroupInstances";
import DeleteIcon from '@mui/icons-material/Delete';
import {MythicConfirmDialog} from '../../MythicComponents/MythicConfirmDialog';
import RestoreFromTrashIcon from '@mui/icons-material/RestoreFromTrash';
import PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwoTone';
import RuleTwoToneIcon from '@mui/icons-material/RuleTwoTone';
import ChecklistRtlTwoToneIcon from '@mui/icons-material/ChecklistRtlTwoTone';
import {EventGroupTableRunAsDialog} from "./EventApprovalDialog";
import AttachmentIcon from '@mui/icons-material/Attachment';
import Badge from '@mui/material/Badge';
import {EventFileManageDialog} from "./EventFileManageDialog";
import SpellcheckIcon from '@mui/icons-material/Spellcheck';
import {EventTriggerKeywordDialog} from "./EventTriggerKeywordDialog";
import LayersTwoToneIcon from '@mui/icons-material/LayersTwoTone';
import {EventGroupConsumingContainersDialog} from "./EventGroupConsumingContainersDialog";
import CalendarMonthTwoToneIcon from '@mui/icons-material/CalendarMonthTwoTone';
const updateDeleteStatusMutation = gql(`
mutation updateDeleteStatusMutation($eventgroup_id: Int!, $deleted: Boolean!) {
eventingTriggerUpdate(eventgroup_id: $eventgroup_id, deleted: $deleted) {
deleted
}
}
`)
const updateActiveStatusMutation = gql(`
mutation updateActiveStatusMutation($eventgroup_id: Int!, $active: Boolean!) {
eventingTriggerUpdate(eventgroup_id: $eventgroup_id, active: $active) {
active
}
}
`)
const eventingTriggerManualMutation = gql(`
mutation eventingManualTrigger($eventgroup_id: Int!){
eventingTriggerManual(eventgroup_id: $eventgroup_id){
status
error
}
}
`)
export function EventGroupTable({selectedEventGroup, me}) {
const [openEventStepRender, setOpenEventStepRender] = React.useState(false);
const [openEnvView, setOpenEnvView] = React.useState(false);
const [openTriggerDataView, setOpenTriggerDataView] = React.useState(false);
const [openFileView, setOpenFileView] = React.useState(false);
const [openDeleteDialog, setOpenDeleteDialog] = React.useState(false);
const [openActiveDialog, setOpenActiveDialog] = React.useState(false);
const [openApprovalDialog, setOpenApprovalDialog] = React.useState(false);
const [updateDeleteMutation] = useMutation(updateDeleteStatusMutation, {
onCompleted: (data) => {
snackActions.success("Updated event group deleted status");
},
onError: (data) => {
console.log(data);
}
})
const [updateActiveMutation] = useMutation(updateActiveStatusMutation, {
onCompleted: (data) => {
snackActions.success("Updated event group active status");
},
onError: (data) => {
console.log(data);
}
})
const [triggerManually] = useMutation(eventingTriggerManualMutation, {
onCompleted: (data) => {
if(data.eventingTriggerManual.status === "success"){
snackActions.info("Successfully sent trigger message");
} else {
snackActions.error(data.eventingTriggerManual.error);
}
},
onError: (data) => {
}
})
const [selectedInstanceID, setSelectedInstanceID] = React.useState(0);
const [openFileManageView, setOpenFileManageView] = React.useState(false);
const [openTriggerKeyword, setOpenTriggerKeyword] = React.useState(false);
const [consumingContainersErrors, setConsumingContainersErrors] = React.useState(0);
const [openConsumingContainerDialog, setOpenConsumingContainerDialog] = React.useState(false);
React.useEffect( () => {
if(selectedEventGroup?.id > 0){
let consumingContainersErrors = 0;
selectedEventGroup?.eventgroupconsumingcontainers?.forEach( c => {
if(c?.consuming_container === null){
consumingContainersErrors += 1;
}else {
if(!c?.consuming_container?.container_running){
consumingContainersErrors += 1;
}
if(!c?.all_functions_available){
consumingContainersErrors += 1;
}
}
});
setConsumingContainersErrors(consumingContainersErrors);
} else {
setConsumingContainersErrors(0);
setOpenConsumingContainerDialog(false);
}
setSelectedInstanceID(0);
}, [selectedEventGroup])
const onAcceptActive = () => {
updateActiveMutation({variables: {eventgroup_id: selectedEventGroup.id, active: !selectedEventGroup.active}});
}
const onAcceptDelete = () => {
updateDeleteMutation({variables: {eventgroup_id: selectedEventGroup.id, deleted: !selectedEventGroup.deleted}});
}
const onTriggerManual = () => {
triggerManually({variables: {eventgroup_id: selectedEventGroup.id}});
}
return (
<div style={{marginLeft: "20px", display: "flex", overflowY: "auto", flexDirection: "column", height: "100%"}}>
{selectedEventGroup.id === 0 &&
<Typography variant={"h4"}><strong>All Eventing Runs</strong></Typography>
}
{selectedEventGroup.id !== 0 &&
<div>
{selectedEventGroup.deleted ? (
<div style={{float: "right", cursor: "pointer", paddingTop: "5px"}}>
<MythicStyledTooltip title={"Restore event group workflow"}>
<Button color={"secondary"} size="small" onClick={onAcceptDelete} >
<RestoreFromTrashIcon color="success"/>
Restore
</Button>
</MythicStyledTooltip>
</div>
) : (
<div style={{float: "right", cursor: "pointer", paddingTop: "5px"}}>
<MythicStyledTooltip
title={"Mark event group as deleted so no new triggers for it will work and it will be removed from view"}>
<Button color={"secondary"} size="small" onClick={() => {
setOpenDeleteDialog(true);
}} >
<DeleteIcon color="error"/> Delete
</Button>
</MythicStyledTooltip>
{openDeleteDialog &&
<MythicConfirmDialog onClose={() => {
setOpenDeleteDialog(false);
}} onSubmit={onAcceptDelete} open={openDeleteDialog}/>
}
</div>
)}
<Typography variant={"h4"} style={{
display: "inline-block",
marginRight: "10px"
}}><strong>{selectedEventGroup?.name}</strong></Typography>
{selectedEventGroup.active ?
(
<>
<MythicStyledTooltip title={"Disable Event Group"}>
<Button color={"secondary"} onClick={() => {
setOpenActiveDialog(true);
}}>
<NotificationsActiveTwoToneIcon size={"small"} color={"success"}/>
Enabled
</Button>
</MythicStyledTooltip>
{openActiveDialog &&
<MythicConfirmDialog onClose={() => {
setOpenActiveDialog(false);
}} onSubmit={onAcceptActive} open={openActiveDialog}
acceptText={"Disable"}
/>
}
</>
) :
(
<MythicStyledTooltip title={"Enable Event Group"}>
<Button color={"secondary"} onClick={onAcceptActive}>
<NotificationsOffTwoToneIcon size={"small"} color={"warning"}/>
Disabled
</Button>
</MythicStyledTooltip>
)
}
<br/><Typography variant={"h8"}>{selectedEventGroup?.description}</Typography>
<Table>
<TableHead>
<TableRow>
<TableCell>Author</TableCell>
<TableCell style={{width: "12rem"}}>Created At</TableCell>
<TableCell>Trigger</TableCell>
<TableCell>Keywords</TableCell>
<TableCell style={{width: "3rem"}}>Context</TableCell>
<TableCell style={{width: "2rem"}}>Env</TableCell>
<TableCell>Run As</TableCell>
<TableCell style={{width: "12rem"}}>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<MythicTableCell>{selectedEventGroup?.operator?.username}</MythicTableCell>
<MythicTableCell>{toLocalTime(selectedEventGroup?.created_at, me?.user?.view_utc_time)}</MythicTableCell>
<MythicTableCell>{selectedEventGroup.trigger}
{selectedEventGroup.trigger === "cron" &&
<div style={{display: "flex", flexDirection: "row", alignItems: "center"}}>
<CalendarMonthTwoToneIcon style={{marginRight: "10px"}}/>
{toLocalTime(selectedEventGroup.next_scheduled_run, me?.user?.view_utc_time)}
</div>
}
</MythicTableCell>
<MythicTableCell
style={{breakWord: "break-all"}}>{selectedEventGroup.keywords.length > 0 && (
<>{""}{selectedEventGroup.keywords?.join(", ")}</>
)}
</MythicTableCell>
<MythicTableCell >
{selectedEventGroup.trigger_data &&
<IconButton onClick={() => {
setOpenTriggerDataView(true);
}} size={"small"} variant={"outlined"} color={"info"} style={{}}>
<InfoTwoToneIcon/>
</IconButton>
}
</MythicTableCell>
<MythicTableCell>
{selectedEventGroup.environment &&
<IconButton onClick={() => {
setOpenEnvView(true);
}} size={"small"} variant={"outlined"} color={"info"} style={{}}>
<InfoTwoToneIcon/>
</IconButton>
}
</MythicTableCell>
<MythicTableCell>
<div style={{display: "flex", alignItems: "center"}}>
{selectedEventGroup.approved_to_run ?
(
<MythicStyledTooltip title={"all user approvals received"}>
<IconButton onClick={() => setOpenApprovalDialog(true)}>
<ChecklistRtlTwoToneIcon color={"success"} />
</IconButton>
</MythicStyledTooltip>
) : (
<MythicStyledTooltip title={"missing user approvals"}>
<IconButton onClick={() => setOpenApprovalDialog(true)}>
<RuleTwoToneIcon color={"error"} />
</IconButton>
</MythicStyledTooltip>
)
}
{selectedEventGroup.run_as}
</div>
</MythicTableCell>
<MythicTableCell>
{selectedEventGroup.trigger === "manual" &&
<MythicStyledTooltip title={"Trigger manually now"}>
<IconButton onClick={onTriggerManual}>
<PlayCircleFilledTwoToneIcon color={"success"}/>
</IconButton>
</MythicStyledTooltip>
}
<MythicStyledTooltip title={"Large Graph View"}>
<IconButton onClick={() => {
setOpenEventStepRender(true);
}} size={"small"} variant={"outlined"} color={"secondary"} style={{}}>
<OpenInNewTwoToneIcon color={"info"} style={{cursor: "pointer", marginRight: "5px"}}/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"View Original File"}>
<IconButton onClick={() => {
setOpenFileView(true);
}} size={"small"} color={"secondary"} style={{}}>
<PermMediaTwoToneIcon color={"info"} style={{cursor: "pointer", marginRight: "5px"}}/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Manage Associated Files"}>
<IconButton onClick={() => {
setOpenFileManageView(true);
}} size={"small"} variant={"outlined"} color={"secondary"} style={{}}>
<Badge badgeContent={selectedEventGroup.filemeta.length} color="secondary">
<AttachmentIcon color={"info"} style={{cursor: "pointer", marginRight: "5px"}}/>
</Badge>
</IconButton>
</MythicStyledTooltip>
{selectedEventGroup.keywords.length > 0 &&
<MythicStyledTooltip title={"Manually execute via keyword"}>
<IconButton onClick={() => {
setOpenTriggerKeyword(true);
}} size={"small"} variant={"outlined"} color={"secondary"} style={{}}>
<SpellcheckIcon color={"success"} style={{cursor: "pointer", marginRight: "5px"}}/>
</IconButton>
</MythicStyledTooltip>
}
{selectedEventGroup.eventgroupconsumingcontainers.length > 0 &&
<MythicStyledTooltip title={"View details about associated eventing containers"} >
<Badge badgeContent={consumingContainersErrors} color={"error"}>
<IconButton onClick={() => {
setOpenConsumingContainerDialog(true);
}} size={"small"} variant={"outlined"} color={"secondary"}>
<LayersTwoToneIcon color={consumingContainersErrors > 0 ? "error": "info"} style={{marginRight: "-3px"}} />
</IconButton>
</Badge>
</MythicStyledTooltip>
}
</MythicTableCell>
</TableRow>
</TableBody>
</Table>
</div>
}
<RenderSteps selectedInstanceID={selectedInstanceID} selectedEventGroup={selectedEventGroup} />
<EventGroupInstances setSelectedInstance={setSelectedInstanceID}
selectedInstanceID={selectedInstanceID}
selectedEventGroup={selectedEventGroup} me={me}/>
{openEventStepRender &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openEventStepRender}
onClose={() => {
setOpenEventStepRender(false);
}}
innerDialog={<EventStepRenderDialog onClose={() => {
setOpenEventStepRender(false);
}} selectedEventGroup={selectedEventGroup}/>}
/>
}
{openApprovalDialog &&
<MythicDialog fullWidth={true} maxWidth="md" open={openApprovalDialog}
onClose={() => {
setOpenApprovalDialog(false);
}}
innerDialog={<EventGroupTableRunAsDialog onClose={() => {
setOpenApprovalDialog(false);
}} eventgroupapprovals={selectedEventGroup.eventgroupapprovals}
me={me} selectedEventGroup={selectedEventGroup} />}
/>
}
{openFileView &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openFileView}
onClose={(e) => {
setOpenFileView(false);
}}
innerDialog={<PreviewFileMediaDialog
agent_file_id={selectedEventGroup.filemetum.agent_file_id}
filename={b64DecodeUnicode(selectedEventGroup.filemetum.filename_text)}
onClose={(e) => {
setOpenFileView(false);
}}/>}
/>
}
{openEnvView && <MythicDialog fullWidth={true} maxWidth="md" open={openEnvView}
onClose={() => {
setOpenEnvView(false);
}}
innerDialog={<MythicViewJSONAsTableDialog
title="View Global Environment Settings" leftColumn="Env Key"
rightColumn="Env Value" value={selectedEventGroup.environment}
onClose={() => {
setOpenEnvView(false);
}}/>}
/>
}
{openTriggerDataView && <MythicDialog fullWidth={true} maxWidth="md" open={openTriggerDataView}
onClose={() => {
setOpenTriggerDataView(false);
}}
innerDialog={<MythicViewJSONAsTableDialog
title="View Trigger context"
leftColumn="Context Key" rightColumn="Context Value"
value={selectedEventGroup.trigger_data}
onClose={() => {
setOpenTriggerDataView(false);
}}/>}
/>
}
{openFileManageView &&
<MythicDialog fullWidth={true} maxWidth="md" open={openFileManageView}
onClose={() => {
setOpenFileManageView(false);
}}
innerDialog={<EventFileManageDialog onClose={() => {
setOpenFileManageView(false);
}} me={me} selectedEventGroup={selectedEventGroup} />}
/>
}
{openTriggerKeyword &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openTriggerKeyword}
onClose={() => {
setOpenTriggerKeyword(false);
}}
innerDialog={<EventTriggerKeywordDialog onClose={() => {
setOpenTriggerKeyword(false);
}} me={me} selectedEventGroup={selectedEventGroup} />}
/>
}
{openConsumingContainerDialog &&
<MythicDialog fullWidth={true} maxWidth="md" open={openConsumingContainerDialog}
onClose={() => {
setOpenConsumingContainerDialog(false);
}}
innerDialog={<EventGroupConsumingContainersDialog onClose={() => {
setOpenConsumingContainerDialog(false);
}} selectedEventGroup={selectedEventGroup} />}
/>
}
</div>
)
}
function RenderSteps({selectedEventGroup, selectedInstanceID}){
const theme = useTheme();
const getRenderer = () => {
if(selectedInstanceID > 0){
return <EventStepInstanceRenderFlowWithProvider selectedEventGroupInstance={selectedInstanceID} />
}
if(selectedEventGroup.id > 0){
return <EventStepRenderFlowWithProvider selectedEventGroup={selectedEventGroup} />
}
return null
}
return (
<div style={{
height: "200px",
minHeight: "200px",
maxHeight: "200px",
border: `1px solid ${theme.palette.secondary.main}`,
borderRadius: "5px"
}}>
{getRenderer()}
</div>
)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,181 @@
import React from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import {useMutation, gql} from '@apollo/client';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import {IconButton} from '@mui/material';
import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select';
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
import DeleteIcon from '@mui/icons-material/Delete';
import {TextField} from '@mui/material';
import {snackActions} from "../../utilities/Snackbar";
const triggerKeywordMutation = gql(`
mutation triggerKeywordMutation($keyword: String!, $keywordEnvData: jsonb!){
eventingTriggerKeyword(keyword: $keyword, keywordEnvData: $keywordEnvData){
status
error
}
}
`);
export function EventTriggerKeywordDialog({onClose, selectedEventGroup}) {
const [selectedKeyword, setSelectedKeyword] = React.useState(selectedEventGroup?.keywords?.[0] || "");
const [dictionaryData, setDictionaryData] = React.useState([]);
const typeOptions = ["text", "number"];
const [triggerKeyword] = useMutation(triggerKeywordMutation, {
onCompleted: (data) => {
if(data.eventingTriggerKeyword.status === "success"){
snackActions.success("Successfully initiated trigger");
onClose();
} else {
snackActions.error(data.eventingTriggerKeyword.error);
}
},
onError: (data) => {
console.log(data);
}
})
const handleKeywordChange = (event) => {
setSelectedKeyword(event.target.value);
}
const addRow = () => {
setDictionaryData([...dictionaryData, {
"type": "text",
"key": "",
"value": "",
}])
}
const changeRowType = (event, index) => {
let newType = event.target.value;
const newDictionaryData = dictionaryData.map( (row, i) => {
if(index === i){
return {key: row["key"], type: newType, value: newType === "number" ? 0 : ""};
}
return {...row};
});
setDictionaryData(newDictionaryData);
}
const removeRow = (index) => {
let newDictionaryData = [...dictionaryData];
newDictionaryData.splice(index, 1);
setDictionaryData(newDictionaryData);
}
const onChangeRowKey = (value, index) => {
const newDictionaryData = dictionaryData.map( (e, i) => {
if(index === i){
return {...e, key: value};
}
return {...e};
});
setDictionaryData(newDictionaryData);
}
const onChangeRowValue = (value, index) => {
const newDictionaryData = dictionaryData.map( (e, i) => {
if(index === i){
return {...e, value: value};
}
return {...e};
});
setDictionaryData(newDictionaryData);
}
const onSubmit = () => {
const keywordEnvData = dictionaryData.reduce( (prev, cur) => {
try{
if(cur.type === "number"){
prev[cur.key] = parseInt(cur.value);
} else {
prev[cur.key] = cur.value;
}
}catch(error){
prev[cur.key] = cur.value;
}
return {...prev};
}, {});
triggerKeyword({variables: {keyword: selectedKeyword, keywordEnvData:keywordEnvData}});
}
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Trigger a workflow by keyword</DialogTitle>
<DialogContent dividers={true} style={{maxHeight: "calc(70vh)"}}>
<DialogContentText>
Send a dictionary of data when triggering this workflow by a specific keyword.
</DialogContentText>
<Select
style={{marginBottom: "10px", width: "100%"}}
value={selectedKeyword}
onChange={handleKeywordChange}
>
{
selectedEventGroup.keywords.map((opt, i) => (
<MenuItem key={"keyword" + opt} value={opt}>{opt}</MenuItem>
))
}
</Select>
<Table>
<TableHead>
<TableRow>
<TableCell style={{width: "50%"}}>Key</TableCell>
<TableCell>Value</TableCell>
</TableRow>
{dictionaryData.map( (e, index) => (
<TableRow key={"dictionarydata" + index}>
<MythicStyledTableCell style={{display: "flex"}} >
<IconButton color={"error"} onClick={() => removeRow(index)} >
<DeleteIcon />
</IconButton>
<Select
style={{}}
value={e.type}
onChange={(evt) => changeRowType(evt, index)}
>
{
typeOptions.map((opt, i) => (
<MenuItem key={"type" + opt} value={opt}>{opt}</MenuItem>
))
}
</Select>
<TextField value={e.key}
style={{width: "100%"}}
onChange={(evt) => onChangeRowKey(evt.target.value, index)} />
</MythicStyledTableCell>
<MythicStyledTableCell>
<TextField value={e.value}
style={{width: "100%"}}
onChange={(evt) => onChangeRowValue(evt.target.value, index)} />
</MythicStyledTableCell>
</TableRow>
))}
</TableHead>
<TableBody>
<TableRow>
<MythicStyledTableCell>
<Button onClick={addRow} color={"success"}>
Add Entry
</Button>
</MythicStyledTableCell>
<MythicStyledTableCell></MythicStyledTableCell>
</TableRow>
</TableBody>
</Table>
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
Close
</Button>
<Button onClick={onSubmit} variant="contained" color="success">
Submit
</Button>
</DialogActions>
</React.Fragment>
);
}
@@ -0,0 +1,268 @@
import React, {useCallback} from 'react';
import { gql, useSubscription, useQuery } from '@apollo/client';
import {snackActions} from '../../utilities/Snackbar';
import {useTheme} from '@mui/material/styles';
import Button from '@mui/material/Button';
import List from '@mui/material/List';
import ListSubheader from '@mui/material/ListSubheader';
import Divider from '@mui/material/Divider';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import {EventGroupTable} from "./EventGroupTable";
import {UploadEventFile} from "../../MythicComponents/MythicFileUpload";
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import VisibilityIcon from '@mui/icons-material/Visibility';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import { IconButton } from '@mui/material';
import NotificationsOffTwoToneIcon from '@mui/icons-material/NotificationsOffTwoTone';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
const get_eventgroups = gql`
query GetEventGroups {
eventgroup(limit: 50, order_by: {id: desc}) {
id
operator {
username
}
filemetum {
agent_file_id
id
filename_text
}
filemeta(where: {deleted: {_eq: false}}) {
agent_file_id
id
filename_text
deleted
}
name
description
trigger
trigger_data
next_scheduled_run
keywords
environment
active
deleted
created_at
run_as
approved_to_run
eventgroupapprovals(order_by: {id: asc}) {
id
operator {
id
username
}
approved
created_at
updated_at
}
eventgroupconsumingcontainers {
id
consuming_container_name
all_functions_available
function_names
consuming_container {
container_running
subscriptions
}
}
}
}
`;
const sub_eventgroups = gql`
subscription GetEventGroups {
eventgroup_stream(cursor: {initial_value: {updated_at: "1970-01-01"}, ordering: ASC}, batch_size: 50, where: {}) {
id
operator {
username
}
filemetum {
agent_file_id
id
filename_text
}
filemeta(where: {deleted: {_eq: false}}) {
agent_file_id
id
filename_text
deleted
}
name
description
trigger
trigger_data
next_scheduled_run
keywords
environment
active
deleted
created_at
run_as
approved_to_run
eventgroupapprovals(order_by: {id: asc}) {
id
operator {
id
username
}
approved
created_at
updated_at
}
eventgroupconsumingcontainers {
id
consuming_container_name
all_functions_available
function_names
consuming_container {
container_running
subscriptions
}
}
}
}
`;
export function Eventing({me}){
const theme = useTheme();
const [eventgroups, setEventgroups] = React.useState([]);
const [showDeleted, setShowDeleted] = React.useState(false);
const [selectedEventGroup, setSelectedEventGroup] = React.useState({id: 0});
const foundQueryEvent = React.useRef(false);
useQuery(get_eventgroups, {
fetchPolicy: "no-cache",
onCompleted: (data) => {
setEventgroups( function(prevState) {
const newEvents = data.eventgroup.reduce( (prev, cur) => {
let indx = prev.findIndex( ({id}) => id === cur.id);
if(indx > -1){
let updatingPrev = [...prev];
updatingPrev[indx] = cur;
return [...updatingPrev];
}
return [...prev, cur];
}, [...prevState]);
newEvents.sort((a,b) => (a.id > b.id) ? -1 : ((b.id > a.id) ? 1 : 0));
if(selectedEventGroup.id > 0){
const updatedSelectedEventGroup = newEvents.filter(e => e.id === selectedEventGroup.id);
if(updatedSelectedEventGroup.length > 0){
setSelectedEventGroup(updatedSelectedEventGroup[0]);
}
}
return newEvents;
})
},
onError: (error) => {
console.log(error);
}
})
useSubscription(sub_eventgroups, {
fetchPolicy: "no-cache",
onData: ({data}) => {
setEventgroups( function(prevState) {
const newEvents = data.data.eventgroup_stream.reduce( (prev, cur) => {
let indx = prev.findIndex( ({id}) => id === cur.id);
if(indx > -1){
let updatingPrev = [...prev];
updatingPrev[indx] = cur;
return [...updatingPrev];
}
return [...prev, cur];
}, [...prevState]);
newEvents.sort((a,b) => (a.id > b.id) ? -1 : ((b.id > a.id) ? 1 : 0));
if(selectedEventGroup.id > 0){
const updatedSelectedEventGroup = newEvents.filter(e => e.id === selectedEventGroup.id);
if(updatedSelectedEventGroup.length > 0){
setSelectedEventGroup(updatedSelectedEventGroup[0]);
}
}
return newEvents;
})
}
});
const onFileChange = async (evt) => {
for(let i = 0; i < evt.target.files.length; i++){
let uploadStatus = await UploadEventFile(evt.target.files[i], "New Eventing Workflow");
if(uploadStatus.status === "error"){
snackActions.error(uploadStatus.error);
}
}
evt.target.value = null;
}
React.useEffect( () => {
if( !foundQueryEvent.current ){
let queryParams = new URLSearchParams(window.location.search);
const eventgroup = queryParams.has("eventgroup") ? queryParams.get("eventgroup") : "0";
if(eventgroup !== "0"){
let matchedGroup = eventgroups.find( e => `${e.id}` === eventgroup);
if(matchedGroup){
setSelectedEventGroup(matchedGroup);
foundQueryEvent.current = true;
}
}
}
}, [eventgroups]);
return (
<div style={{display: "flex", flexDirection: "column", height: "100%", overflowY: "auto"}}>
<div style={{display: "flex", flexDirection: "row", height: "100%"}}>
<div style={{width: "20%", borderRight: "2px solid grey", height: '100%', display: "flex", flexDirection: "column"}}>
<Button size={"small"} style={{float: "right", marginRight: "10px"}}
variant={"contained"} color={"success"} component="label"
startIcon={<CloudUploadIcon />}
>
New Event Groups
<input onChange={onFileChange} type="file" multiple hidden/>
</Button>
<ListItem button onClick={() => setSelectedEventGroup({id: 0})}
style={selectedEventGroup.id === 0 ?
{paddingTop: 0, paddingBottom: 0, borderLeft: `5px solid ${theme.palette.info.main}`} :
{paddingTop: 0, paddingBottom: 0}}>
<ListItemText primary={"View All Instances"} />
</ListItem>
<div style={{flexGrow: 1, overflowY: "auto", height:"100%"}}>
<List style={{border: 0}}
subheader={
<ListSubheader style={{backgroundColor: theme.palette.secondary.main,
color: theme.palette.text.contrast,
lineHeight: "30px"}} component="div">
Registered Event Groups
{showDeleted ? (
<MythicStyledTooltip title={"Hide Deleted Services"} style={{float: "right"}}>
<IconButton size="small" style={{float: "right", }} variant="contained" onClick={() => setShowDeleted(!showDeleted)}><VisibilityIcon /></IconButton>
</MythicStyledTooltip>
) : (
<MythicStyledTooltip title={"Show Deleted Services"} style={{float: "right"}}>
<IconButton size="small" style={{float: "right", }} variant="contained" onClick={() => setShowDeleted(!showDeleted)} ><VisibilityOffIcon /></IconButton>
</MythicStyledTooltip>
)}
</ListSubheader>
}>
{eventgroups.map( (e, i) => (
(showDeleted || !e.deleted) &&
<ListItem key={e.id + e.name} button onClick={() => setSelectedEventGroup(e)}
style={selectedEventGroup.id === e.id ?
{paddingTop: 0, paddingBottom: 0, borderLeft: `5px solid ${theme.palette.info.main}`} :
{paddingTop: 0, paddingBottom: 0}}>
{!e.active &&
<NotificationsOffTwoToneIcon color={"warning"} />
}
<ListItemText primary={e.name} style={{
textDecoration: e.deleted ? "line-through" : ""
}} />
</ListItem>
))}
</List>
</div>
<Divider />
</div>
<div style={{width: "80%", height: "100%"}}>
<EventGroupTable selectedEventGroup={selectedEventGroup} me={me} />
</div>
</div>
</div>
)
}
@@ -18,11 +18,13 @@ query GetC2AndPayloadType {
c2profile(where: {deleted: {_eq: false}}) {
name
id
container_running
}
payloadtype(where: {deleted: {_eq: false}}) {
payloadtype(where: {deleted: {_eq: false}, agent_type: {_eq: "agent"}}) {
name
wrapper
id
container_running
payloadtypec2profiles {
c2profile {
name
@@ -30,10 +32,11 @@ query GetC2AndPayloadType {
}
}
}
wrappers: payloadtype(where: {deleted: {_eq: false}, wrapper: {_eq: true}}) {
wrappers: payloadtype(where: {deleted: {_eq: false}, agent_type: {_eq: "agent"}, wrapper: {_eq: true}}) {
name
wrapper
id
container_running
wrap_these_payload_types {
wrapped {
wrapper
@@ -106,7 +106,6 @@ function getTaskStatusNormalized (taskStatus) {
if(status.includes("opsec")){
return "opsec"
}
console.log(status);
return "submitted"
@@ -1008,7 +1007,7 @@ const LineTimeMultiChartCard = ({data, additionalStyles, colors=cheerfulFiestaPa
return (
<Paper elevation={5} style={{
marginBottom: "5px",
marginTop: "10px",
marginTop: "5px",
width: "100%",
height: "100%",
border: "1px solid gray",
@@ -1022,7 +1021,6 @@ const LineTimeMultiChartCard = ({data, additionalStyles, colors=cheerfulFiestaPa
xAxis={[
{
data: data.x,
//valueFormatter: (v) => tooltipDate(v, view_utc_time),
scaleType: "time",
min: data?.x?.[value[0]] || 0,
max: data?.x?.[value[1]] || 0,
@@ -0,0 +1,99 @@
import React from 'react';
import {Button} from '@mui/material';
import MythicTextField from '../../MythicComponents/MythicTextField';
import logo from '../../../assets/mythic-red.png';
import { Navigate } from 'react-router-dom';
import {restartWebsockets} from '../../../index';
import { snackActions } from '../../utilities/Snackbar';
import CardContent from '@mui/material/CardContent';
export function InviteForm(props){
const [username, setUsername] = React.useState("");
const [password, setPassword] = React.useState("");
const [email, setEmail] = React.useState("");
let queryParams = new URLSearchParams(window.location.search);
const suppliedCode = queryParams.has("code") ? queryParams.get("code") : "";
const [inviteCode, setInviteCode] = React.useState(suppliedCode);
const submit = e => {
e.preventDefault();
if( username === "" || password === ""){
snackActions.warning("Username and Password required");
return;
}
const requestOptions = {
method: "POST",
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username, password, code: inviteCode, email})
};
fetch('/invite', requestOptions).then((response) => {
if(response.status !== 200){
snackActions.warning("HTTP " + response.status + " Error: Check Mythic logs");
return;
}
response.json().then(data => {
//console.log(data)
if(data["status"] === "success"){
snackActions.success("Successfully registered new account!");
restartWebsockets();
window.location = "/new/login";
}else{
snackActions.warning(data["error"]);
console.log("Error", data);
}
}).catch(error => {
snackActions.warning("Error getting JSON from server: " + error.toString());
console.log("Error trying to get json response", error, response);
});
}).catch(error => {
if(error.toString() === "TypeError: Failed to fetch"){
snackActions.warning("Please refresh and accept the SSL connection error");
} else {
snackActions.warning("Error talking to server: " + error.toString());
}
console.log("There was an error!", error);
});
}
const onChangeText = (name, value, error) => {
if(name === "username"){
setUsername(value);
}else if(name === "password"){
setPassword(value);
}else if(name === "code"){
setInviteCode(value);
}else if(name === "email"){
setEmail(value);
}
}
return (
<div style={{justifyContent: "center", display: "flex"}}>
{
props.me.loggedIn ?
(
<Navigate replace to={"/"}/>
)
: (
<div style={{backgroundColor: "transparent"}}>
<CardContent>
<img src={logo} height="400px" alt="Mythic logo"/>
<form onSubmit={submit}>
<MythicTextField name='code' value={inviteCode}
onChange={onChangeText} width={31}/>
<MythicTextField name='username' value={username} onChange={onChangeText}
width={31}/>
<MythicTextField name='password' type="password" onEnter={submit} value={password}
onChange={onChangeText} width={31}/>
<MythicTextField name='email' value={email} onChange={onChangeText}
width={31}/>
<Button type="submit" color="primary" onClick={submit} variant="contained"
style={{}}>Register</Button>
</form>
</CardContent>
</div>
)
}
</div>
)
}
@@ -1,26 +1,65 @@
import React, { useEffect } from 'react';
import {Button} from '@mui/material';
import {Button, Paper} from '@mui/material';
import MythicTextField from '../../MythicComponents/MythicTextField';
import logo from '../../../assets/mythic@2x.png';
import logo from '../../../assets/mythic-red.png';
import { Navigate } from 'react-router-dom';
import { meState, successfulLogin, FailedRefresh } from '../../../cache';
import { useReactiveVar } from '@apollo/client';
import {restartWebsockets, isJWTValid} from '../../../index';
import { snackActions } from '../../utilities/Snackbar';
import CardContent from '@mui/material/CardContent';
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';
export function LoginForm(props){
const me = useReactiveVar(meState);
const [openUpdate, setOpenUpdateDialog] = React.useState(false);
const dropdownAnchorRef = React.useRef(null);
const [username, setUsername] = React.useState("");
const [password, setPassword] = React.useState("");
const [requestField, setRequestFields] = React.useState([]);
const [authOptions, setAuthOptions] = React.useState([]);
const selectedAuthOptionRef = React.useRef({});
let queryParams = new URLSearchParams(window.location.search);
const redirectPath = queryParams.has("redirect") ? queryParams.get("redirect") : "/new";
useEffect( () => {
if(!isJWTValid()){
console.log("calling FailedRefresh on the login page to remove old data");
FailedRefresh();
}else{
//console.log(meState());
}
const requestOptions = {
method: "GET",
headers: {'Content-Type': 'application/json', MythicSource: "web"},
};
fetch('/auth_services', requestOptions).then((response) => {
if(response.status !== 200){
snackActions.warning("HTTP " + response.status + " Error: Check Mythic logs");
return;
}
response.json().then(data => {
if(data['status'] === 'success'){
setAuthOptions(data["idps"]);
} else {
snackActions.warning("Failed to fetch potential additional auth methods: " + data["error"]);
}
}).catch(error => {
snackActions.warning("Error getting JSON from server: " + error.toString());
console.log("Error trying to get json response", error, response);
});
}).catch(error => {
if(error.toString() === "TypeError: Failed to fetch"){
snackActions.warning("Please refresh and accept the SSL connection error");
} else {
snackActions.warning("Error talking to server: " + error.toString());
}
console.log("There was an error!", error);
});
}, [])
// normal username + password to Mythic for local Mythic auth
const submit = e => {
e.preventDefault();
if( username === "" || password === ""){
@@ -29,7 +68,7 @@ export function LoginForm(props){
}
const requestOptions = {
method: "POST",
headers: {'Content-Type': 'application/json'},
headers: {'Content-Type': 'application/json', MythicSource: "web"},
body: JSON.stringify({username, password})
};
fetch('/auth', requestOptions).then((response) => {
@@ -59,36 +98,167 @@ export function LoginForm(props){
console.log("There was an error!", error);
});
}
const submitNonIDP = e => {
e.preventDefault();
const finalRequestFields = requestField.reduce( (prev, cur) => {
return {...prev, [cur.name]: cur.value}
}, {});
const requestOptions = {
method: "POST",
headers: {'Content-Type': 'application/json', MythicSource: "web"},
body: JSON.stringify({"request_fields": finalRequestFields})
};
fetch(`/auth_acs/${selectedAuthOptionRef.current.container}/${selectedAuthOptionRef.current.idp}`, requestOptions).then((response) => {
if(response.redirected){
window.location.href = response.url;
} else {
snackActions.warning("Failed to authenticate");
}
}).catch(error => {
snackActions.warning("Error getting JSON from server: " + error.toString());
console.log("Error trying to get json response", error);
});
}
const onUpdateText = (name, value, error) => {
const updatedRequestFields = requestField.map( r => {
if(r.name === name){
return {...r, value: value};
}
return {...r};
});
setRequestFields(updatedRequestFields);
}
const onUsernameChange = (name, value, error) => {
setUsername(value);
}
const onPasswordChange = (name, value, error) => {
setPassword(value);
}
const redirectPath = () => {
return '/new/';
const resetLoginOption = () => {
setRequestFields([]);
}
const handleMenuItemClick = (event, index) => {
setOpenUpdateDialog(false);
snackActions.info("Fetching IDP Information");
const requestOptions = {
method: "GET",
headers: {'Content-Type': 'application/json', MythicSource: "web"},
};
let authOptionsPieces = authOptions[index].split(" - ");
selectedAuthOptionRef.current = {"container": authOptionsPieces[1], "idp": authOptionsPieces[0]};
fetch(`/auth_redirect/${authOptionsPieces[1]}/${authOptionsPieces[0]}`, requestOptions).then((response) => {
if(response.status !== 200){
snackActions.warning("HTTP " + response.status + " Error: Check Mythic logs");
return;
}
response.json().then(data => {
if(data["status"] === "success"){
if(data["redirect"]){
window.location = data["redirect"];
} else if(data["request_fields"]){
let values = data["request_fields"].map(r => {return {"name": r, "value": ""}})
setRequestFields(values);
snackActions.info("Updating to " + authOptions[index]);
}
} else {
snackActions.warning("Failed to get redirect url: " + data["error"]);
}
}).catch(error => {
snackActions.warning("Error getting JSON from server: " + error.toString());
console.log("Error trying to get json response", error, response);
});
}).catch(error => {
if(error.toString() === "TypeError: Failed to fetch"){
snackActions.warning("Please refresh and accept the SSL connection error");
} else {
snackActions.warning("Error talking to server: " + error.toString());
}
console.log("There was an error!", error);
});
}
const handleClose = (event) => {
if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
return;
}
setOpenUpdateDialog(false);
};
return (
<div style={{justifyContent: "center", display: "flex"}}>
{
me.loggedIn ?
(
<Navigate replace to={redirectPath()}/>
<Navigate replace to={redirectPath}/>
)
: (
<div style={{backgroundColor: "transparent"}}>
<CardContent>
<CardContent >
<img src={logo} height="400px" alt="Mythic logo"/>
<form onSubmit={submit}>
<MythicTextField name='username' value={username} onChange={onUsernameChange} width={30} />
<MythicTextField name='password' type="password" onEnter={submit} value={password} onChange={onPasswordChange} width={30} />
<Button type="submit" color="primary" onClick={submit} variant="contained" style={{marginRight: "10px"}}>Login</Button>
</form>
{requestField.length === 0 &&
<>
<MythicTextField name='username' value={username} onChange={onUsernameChange}
width={31}/>
<MythicTextField name='password' type="password" onEnter={submit} value={password}
onChange={onPasswordChange} width={31}/>
<Button type="submit" color="primary" onClick={submit} variant="contained"
style={{}}>Login</Button>
</>
}
{requestField.length > 0 &&
<>
{requestField.map(r => (
<MythicTextField key={r.name} name={r.name} value={r.value} onChange={onUpdateText}
type={r.name === "password" ? "password" : ""}
width={31}/>
))}
<Button type="submit" color="primary" onClick={submitNonIDP} variant="contained"
style={{}}>Login via {selectedAuthOptionRef.current.idp}</Button>
</>
}
{authOptions.length > 0 &&
<>
<Button ref={dropdownAnchorRef} style={{float: "right"}}
variant={"outlined"}
onClick={() => {
setOpenUpdateDialog(true);
}} color="success"
>
Login via Custom Auth Providers
</Button>
<Popper open={openUpdate} anchorEl={dropdownAnchorRef.current}
role={undefined} transition disablePortal style={{zIndex: 4}}>
{({TransitionProps, placement}) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
}}
>
<Paper className={"dropdownMenuColored"}>
<ClickAwayListener onClickAway={handleClose}
mouseEvent={"onMouseDown"}>
<MenuList id="split-button-menu">
{authOptions.map((option, index) => (
<MenuItem
key={index + option}
onClick={(event) => handleMenuItemClick(event, index)}
>
{option}
</MenuItem>
))}
<MenuItem onClick={resetLoginOption}>Mythic Local Login</MenuItem>
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</>
}
</CardContent>
</div>
)
}
</div>
)
)
}
@@ -24,7 +24,8 @@ export function MitreGrid({entries, onGetCommands, onGetTasks, onGetCommandsFilt
return (
<div style={{display: "flex", flexDirection: "column", width: "100%", height: "100%"}}>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,
marginBottom: "5px", marginLeft: "5px", marginRight: "5px"}} variant={"elevation"}>
<Typography variant="h3" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
{"MITRE ATT&CK Mappings"}
</Typography>
@@ -36,7 +36,7 @@ export function MitreGridColumn({column, showCountGrouping}){
<Box width={"100%"} style={{backgroundColor: theme.tableHover}}>
<h2 style={{margin: 0, textAlign: "center"}}><b>{column.tactic}</b></h2>
<p style={{textAlign: "center", margin: 0}}>{techniqueCounts} techniques</p>
{ showCountGrouping === "" ? (null) : (
{ showCountGrouping === "" ? null : (
<p style={{textAlign: "center", margin: 0}}>{groupCounts} {showCountGrouping}s</p>
)}
</Box>
@@ -32,12 +32,13 @@ export function MitreGridRow({row, showCountGrouping}){
{row?.t_num?.includes(".") ?
(
<Box width={"50px"} height={"1px"} style={{border: "1px dashed grey"}} />
) : ( null )}
) : null}
<Button style={{
width: "100%",
justifyContent: "flex-start",
...buttonColor
}}
}}
color={"secondary"}
variant="outlined"
onClick={() => setOpenDisplay(true)}
>
@@ -48,7 +49,7 @@ export function MitreGridRow({row, showCountGrouping}){
onClose={()=>{setOpenDisplay(false);}}
innerDialog={<MitreMapDisplayDialog entry={row} showCountGrouping={showCountGrouping} onClose={()=>{setOpenDisplay(false);}} />}
/>
) : (null) }
) : null }
</div>
)
@@ -57,7 +57,7 @@ export function MitreMapDisplayDialog({entry, showCountGrouping, onClose}){
}, [entry, showCountGrouping]);
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">{entry.name} - <Link style={{wordBreak: "break-all"}} underline="always" target="_blank" href={"https://attack.mitre.org/techniques/" + entry.t_num.replace(".", "/")} >{entry.t_num}</Link></DialogTitle>
<DialogTitle id="form-dialog-title">{entry.name} - <Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank" href={"https://attack.mitre.org/techniques/" + entry.t_num.replace(".", "/")} >{entry.t_num}</Link></DialogTitle>
<DialogContent dividers={true}>
{showCountGrouping === "command" ?
(
@@ -172,11 +172,16 @@ export function CommandBlockListTable(props){
}
return (
<React.Fragment>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main, marginBottom: "5px", marginTop: "10px", marginRight: "5px"}} variant={"elevation"}>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,
marginBottom: "5px", marginLeft: "5px", marginRight: "5px"}} variant={"elevation"}>
<Typography variant="h3" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
Command Block Lists
</Typography>
<Button size="small" onClick={()=>{setOpenNewDialog(true);}} style={{marginRight: "20px", float: "right", marginTop: "10px"}} startIcon={<AddCircleOutlineOutlinedIcon/>} color="success" variant="contained">New Block List</Button>
<Button size="small"
onClick={()=>{setOpenNewDialog(true);}}
style={{marginRight: "10px", float: "right", marginTop: "10px"}}
startIcon={<AddCircleOutlineOutlinedIcon color="success"/>}
variant="contained">New Block List</Button>
{openNew &&
<MythicDialog open={openNew} fullWidth={true} maxWidth="lg"
onClose={()=>{setOpenNewDialog(false);}}
@@ -185,12 +190,11 @@ export function CommandBlockListTable(props){
/>
}
</Paper>
<TableContainer component={Paper} className="mythicElement">
<TableContainer className="mythicElement">
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "5rem"}}>Delete</TableCell>
<TableCell style={{width: "2rem"}}></TableCell>
<TableCell style={{width: "7rem"}}>Modify</TableCell>
<TableCell style={{width: "15rem"}}>Name</TableCell>
<TableCell >Blocked Commands</TableCell>
@@ -238,11 +242,11 @@ function CommandBlockListTableRow(props){
return (
<TableRow hover>
<TableCell>
<IconButton size="small" onClick={()=>{setOpenDeleteDialog(true);}} style={{color: theme.palette.error.main}} variant="contained"><DeleteIcon/></IconButton>
<IconButton size="small" onClick={()=>{setOpenDeleteDialog(true);}} color={"error"} variant="contained"><DeleteIcon/></IconButton>
<MythicConfirmDialog onClose={() => {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete} open={openDelete}/>
</TableCell>
<TableCell>
<Button size="small" onClick={()=>{setOpenUpdateDialog(true);}} startIcon={<TuneIcon/>} color="info" variant="contained">Edit</Button>
<Button size="small" onClick={()=>{setOpenUpdateDialog(true);}} startIcon={<TuneIcon/>} color="primary" variant="contained">Edit</Button>
{openUpdate &&
<MythicDialog open={openUpdate} fullWidth maxWidth={"lg"}
onClose={()=>{setOpenUpdateDialog(false);}}

Some files were not shown because too many files have changed in this diff Show More