mirror of
https://github.com/its-a-feature/Mythic
synced 2026-06-08 14:55:38 +00:00
Mythic3.3.0 Initial push
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
Generated
+4765
-2775
File diff suppressed because it is too large
Load Diff
+29
-29
@@ -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 |
@@ -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,
|
||||
|
||||
@@ -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": "",
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
+7
-9
@@ -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);
|
||||
},
|
||||
|
||||
+16
-20
@@ -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>
|
||||
);
|
||||
|
||||
+79
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
+3
-9
@@ -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>
|
||||
|
||||
-1
@@ -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';
|
||||
|
||||
|
||||
|
||||
+20
-15
@@ -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"
|
||||
>
|
||||
>>
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
variant="contained"
|
||||
size="small"
|
||||
className={classes.button}
|
||||
onClick={handleCheckedRight}
|
||||
disabled={leftChecked.length === 0}
|
||||
aria-label="move selected right"
|
||||
>
|
||||
>
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
variant="contained"
|
||||
size="small"
|
||||
className={classes.button}
|
||||
onClick={handleCheckedLeft}
|
||||
disabled={rightChecked.length === 0}
|
||||
aria-label="move selected left"
|
||||
>
|
||||
<
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
variant="contained"
|
||||
size="small"
|
||||
className={classes.button}
|
||||
onClick={handleAllLeft}
|
||||
disabled={right.length === 0}
|
||||
aria-label="move all left"
|
||||
>
|
||||
<<
|
||||
</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"
|
||||
>
|
||||
>>
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
variant="contained"
|
||||
size="small"
|
||||
className={classes.button}
|
||||
onClick={handleCheckedRight}
|
||||
disabled={leftChecked.length === 0}
|
||||
aria-label="move selected right"
|
||||
>
|
||||
>
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
variant="contained"
|
||||
size="small"
|
||||
className={classes.button}
|
||||
onClick={handleCheckedLeft}
|
||||
disabled={rightChecked.length === 0}
|
||||
aria-label="move selected left"
|
||||
>
|
||||
<
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
variant="contained"
|
||||
size="small"
|
||||
className={classes.button}
|
||||
onClick={handleAllLeft}
|
||||
disabled={right.length === 0}
|
||||
aria-label="move all left"
|
||||
>
|
||||
<<
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
+2
-8
@@ -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
Reference in New Issue
Block a user