mirror of
https://github.com/its-a-feature/Mythic.git
synced 2026-11-07 17:22:06 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96b24b48a7 | ||
|
|
bdc722f6ce | ||
|
|
f9e71d0859 | ||
|
|
742d878928 | ||
|
|
01ea8bc896 | ||
|
|
94418bcd52 | ||
|
|
3ae9c5fe18 | ||
|
|
5e85a20d14 | ||
|
|
5f46db6ede | ||
|
|
e37e057a89 | ||
|
|
33a46d7db7 | ||
|
|
540aec0f00 | ||
|
|
29be09eef4 | ||
|
|
a5a1a972d7 | ||
|
|
82648e8241 | ||
|
|
e4da2c887f | ||
|
|
39f44f7dec | ||
|
|
0f51584493 | ||
|
|
dc50ecc0dc | ||
|
|
59211a2fbf | ||
|
|
f8d355a29c | ||
|
|
1da8fdcd2a | ||
|
|
2dcc3b5699 | ||
|
|
8deec59270 | ||
|
|
071abeaf74 | ||
|
|
6eb53da6b5 | ||
|
|
d605a2f82c | ||
|
|
31db999945 | ||
|
|
9ae193be76 | ||
|
|
fc731af819 | ||
|
|
d21dbf293b | ||
|
|
b6d98b1794 | ||
|
|
448a57b82b | ||
|
|
1d54d5ea0c | ||
|
|
4d793d5998 | ||
|
|
60455c26cb | ||
|
|
183f0605c2 | ||
|
|
37d3cc8b1b | ||
|
|
9532b96f7a | ||
|
|
e1806f0fd5 | ||
|
|
fa235b6378 | ||
|
|
b87219b1e9 | ||
|
|
b5459b56c8 | ||
|
|
ca6d7d2ce0 | ||
|
|
6148c5f0fb | ||
|
|
6e14b2a472 | ||
|
|
307df57df8 | ||
|
|
4a4d95f1ca | ||
|
|
ffc5041349 | ||
|
|
ecda245963 | ||
|
|
6ea67c69b9 | ||
|
|
64d32eb116 | ||
|
|
9f7a1ce770 | ||
|
|
4a3818988c | ||
|
|
1ec6318b62 | ||
|
|
60902a6e66 | ||
|
|
ddda1a266c | ||
|
|
8dc9f351d9 | ||
|
|
d7b4d51186 | ||
|
|
d2334b79de | ||
|
|
96df4db36e | ||
|
|
a72dd1b086 | ||
|
|
89bee21894 | ||
|
|
26a4410f0a | ||
|
|
36fb4c58bf | ||
|
|
cfdf0c537d | ||
|
|
f23e123a56 | ||
|
|
5b4c210e6f | ||
|
|
3dee8797da | ||
|
|
33f48baab4 | ||
|
|
3198f304c3 | ||
|
|
09ac733d45 | ||
|
|
3e611494cc | ||
|
|
8be3c084e5 | ||
|
|
3796640835 | ||
|
|
6e23b29f22 | ||
|
|
22e5c489b2 | ||
|
|
8cdf34c3e7 | ||
|
|
b285558fce | ||
|
|
e536f59347 | ||
|
|
4c472f7e3a | ||
|
|
cc77f2b1a3 | ||
|
|
1c18703e96 | ||
|
|
f017661b3d | ||
|
|
98dd30d984 | ||
|
|
eb3cf1053b | ||
|
|
58e2c6de5f | ||
|
|
7dc66c43aa | ||
|
|
9c6afe2d8c | ||
|
|
6c5b1d2a24 | ||
|
|
fc8239eb85 | ||
|
|
180c6e1914 | ||
|
|
59c97d791f | ||
|
|
d9e1d5e6bd | ||
|
|
0f5746b4a3 | ||
|
|
71c03251fb | ||
|
|
f742ab3d0f | ||
|
|
d3ae12a220 | ||
|
|
d9a41572ac | ||
|
|
e454531999 | ||
|
|
45cf6a9cd8 | ||
|
|
5011314ba5 | ||
|
|
af9d3c40e6 | ||
|
|
03e27abdb6 | ||
|
|
e7fde3fcde | ||
|
|
51c0eeb5df | ||
|
|
0f259d1f7d | ||
|
|
d86f0d6227 | ||
|
|
3bb93efa72 | ||
|
|
652234d7e1 | ||
|
|
44d8fd168e | ||
|
|
2e418bbb4d | ||
|
|
87249b2891 | ||
|
|
dbb15fdbb3 | ||
|
|
04ebb6aba7 | ||
|
|
8a2e42c48c | ||
|
|
db682d5020 | ||
|
|
6649279e3e | ||
|
|
6d1778bb3b | ||
|
|
0484154eae | ||
|
|
99f2b6cb2f | ||
|
|
303741ee28 | ||
|
|
37a3f66d68 | ||
|
|
8e43275c27 | ||
|
|
6dfe40b274 | ||
|
|
49e1c62502 | ||
|
|
a739837c95 | ||
|
|
063f70a55d | ||
|
|
fb6c3d8368 | ||
|
|
10f0eb9e7a | ||
|
|
afdd0ec9cc | ||
|
|
2391244baa | ||
|
|
9e0fbcfa3a | ||
|
|
54e6cc5d32 | ||
|
|
c56d92ee52 | ||
|
|
706e8d3ce7 | ||
|
|
29e57f1384 | ||
|
|
bf7de9ad22 | ||
|
|
e3cbdd35e7 | ||
|
|
2ca4ad507e | ||
|
|
43a8a8580c | ||
|
|
fd02fe2c9e | ||
|
|
3a6d9c4842 | ||
|
|
31e297a37f | ||
|
|
b40aed02b8 | ||
|
|
5e031f64b2 | ||
|
|
f6532b127d | ||
|
|
a383fcf8c5 | ||
|
|
307eb0dd72 | ||
|
|
29db86be7c | ||
|
|
04be0065f3 | ||
|
|
8601357379 | ||
|
|
bb3a4b2294 | ||
|
|
44149bdf2b | ||
|
|
1a2bf56c8c | ||
|
|
63867bc27a | ||
|
|
42a7869750 | ||
|
|
8479a2d091 | ||
|
|
c7357c6fee | ||
|
|
e6fd09b069 | ||
|
|
8cc47d518e | ||
|
|
ae2ed75801 | ||
|
|
7f225e51f1 | ||
|
|
c173f083dd | ||
|
|
3e3356e586 | ||
|
|
70a4ca26ec | ||
|
|
64fd83b999 | ||
|
|
180a81111d | ||
|
|
767881fd6c | ||
|
|
4240722b3a | ||
|
|
36ceaaacba | ||
|
|
bf22f85661 | ||
|
|
e7405bd62c | ||
|
|
2ab6e20162 |
No files matched your search
@@ -167,3 +167,6 @@ _ReSharper*/
|
||||
jupyter-docker/jupyter/MythicExamples/.ipynb_checkpoints/
|
||||
|
||||
nginx-docker/config/blockips.conf
|
||||
/MythicReactUI/build/
|
||||
/MythicReactUI/node_modules/
|
||||
/.gocache/
|
||||
+211
@@ -4,6 +4,217 @@ 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.4.36] - 2026-06-21
|
||||
|
||||
### Changed
|
||||
|
||||
- Addressed 3 issues from GitHub about spectators and overly scoped hasura filters
|
||||
|
||||
## [3.4.35] - 2026-04-16
|
||||
|
||||
### Changed
|
||||
|
||||
- Merged PR #546 to address missing return in GenerateAPITokenWebhook that would allow non-admin users to generate API Tokens for Bot accounts
|
||||
- Added server name to emitted log messages
|
||||
- Merged PR #545 to address missing operation_id in task's log messages for logging containers
|
||||
- Updated C2 profile code in the UI to properly handle `ui_position` from C2 Profile parameters in payload build
|
||||
|
||||
## [3.4.34] - 2026-04-13
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated way that SendAllOperationsMessage functions to reduce duplicate messages to event log
|
||||
- Reduced the number of lookups per error message to increment count by caching error lookup data
|
||||
- Updated event log messages to ensure proper UTF8 encoding (and removal of bad characters)
|
||||
- Using channels to ensure no duplicate messages are created due to go concurrency
|
||||
|
||||
## [3.4.33] - 2026-04-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue where payload/command syncing would error out and leave dangling database connections
|
||||
- Updated the postgres docker image to use customized database info for health checks
|
||||
|
||||
## [3.4.32] - 2026-04-06
|
||||
|
||||
### Changed
|
||||
|
||||
- Merged PR to add GET route for /auth_acs/:containerName/:IDPName that was POST only
|
||||
|
||||
## [3.4.31] - 2026-03-23
|
||||
|
||||
### Changed
|
||||
|
||||
- Removed code in `upload` to overwrite uploaded filename with destination filename in filemeta tracking
|
||||
- Updated `download` code to only write files to disk on new chunks for speed improvements
|
||||
- Updated the `rm` tracking to not blanket wildcard paths, but first check if it's a directory and make sure there's a trailing slash before wildcarding
|
||||
|
||||
## [3.4.30] - 2026-03-20
|
||||
|
||||
### Changed
|
||||
|
||||
- Added a `received_chunk_ids` map to the database for `filemeta` to track not just chunks, but which chunks are received
|
||||
- this helps prevent duplicate chunks getting counted multiple times and causing faulty downloads
|
||||
- Added some default values for `DisplayPath` that were causing null constraint errors on the database
|
||||
- Fixed a remote path null value with uploading eventing files
|
||||
|
||||
## [3.4.29] - 2026-03-18
|
||||
|
||||
### Changed
|
||||
|
||||
- Adjusted the MythicRPCFileSearch function to not return tracked copies of existing files
|
||||
|
||||
## [3.4.28] - 2026-03-17
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a bug where file transfers would fail if no remote path supplied
|
||||
|
||||
## [3.4.27] - 2026-03-16
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a bug where mythic would try to write to a null message after processing a bad message
|
||||
|
||||
## [3.4.26] - 2026-03-12
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a bug in "Callback Imports" that wouldn't recognize exising build parameter types
|
||||
|
||||
## [3.4.25] - 2026-03-04
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated rpfwd to do burst proxy reads similar to socks
|
||||
|
||||
## [3.4.24] - 2026-02-18
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated PushC2 to funnel traffic into one channel instead of two
|
||||
- Updated socks proxy reads to do a basic burst for read sizes to reduce number of concurrent waiting messages
|
||||
- this increases throughput for socks traffic with a bit more bursty memory usage
|
||||
- Updated key reflection in agent messages to omit cwd and impersonation_context fields
|
||||
- Updated empty BFS paths to be cached as well so non-existent paths aren't recomputed a bunch
|
||||
|
||||
## [3.4.23] - 2026-02-08
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the payload type syncing to resync for all payload types, not just new wrappers
|
||||
- this fixes a bug where payload types syncing for the first time after their wrapper synced wouldn't match them up
|
||||
|
||||
## [3.4.22] - 2026-02-02
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue where building payloads while c2 profiles were syncing would double issue tasks to c2 profile and hang
|
||||
- Fixed an issue here cached p2p paths weren't getting flushed properly
|
||||
|
||||
## [3.4.21] - 2026-01-14
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue where downloaded files weren't having their sizes reflected after completion in the file browser
|
||||
|
||||
## [3.4.20] - 2026-01-08
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated IterateAndAct to treat a crypto value of "" the same as "none"
|
||||
|
||||
## [3.4.19] - 2025-12-31
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated processing of skipped steps to indicate skip instead of most recent step that was updated
|
||||
|
||||
## [3.4.18] - 2025-12-23
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated how processing happens for the `update_deleted` with file browser data
|
||||
- data is now processed as part of two channels to ensure ordering and supporting multi-chunk file browser data
|
||||
|
||||
## [3.4.17] - 2025-12-23
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue where uploaded files were getting treated as folders in the file browsers
|
||||
- Fixed an issue where uploaded files would get the wrong timestamp in the file browser
|
||||
- Updated go modules
|
||||
|
||||
## [3.4.16] - 2025-12-23
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue where supplying a cron job in eventing as something other than string would crash due to type conversion
|
||||
|
||||
## [3.4.15] - 2025-12-22
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue with translation containers sometimes not getting associated with their payload types on first sync
|
||||
|
||||
## [3.4.14] - 2025-12-18
|
||||
|
||||
### Changed
|
||||
|
||||
- Added database triggers for `filemeta`, `credential`, and `keylog` to update timestamp when other fields change
|
||||
|
||||
## [3.4.13] - 2025-12-15
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated MythicRPCTaskCreate to require OperatorID, TaskID, or EventStepInstanceID
|
||||
- This is an update to make sure the new task is associated with the right user and not the callback creator
|
||||
- Updated the default tasking to Split Tasking instead of the collapsable accordian tasking
|
||||
|
||||
## [3.4.12] - 2025-12-11
|
||||
|
||||
### Changed
|
||||
|
||||
- Added more context to the Proxy start event log message
|
||||
- Added a new event log message when a Proxy stops
|
||||
- Updated the CustomBrowserExportFunctionMessage to also take in a callback group
|
||||
- Updated the SendMythicRPCFileCreate call to also allow creation by OperationID
|
||||
|
||||
## [3.4.11] - 2025-12-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Added `CustomBrowser` containers to allow customized file-like browser creation
|
||||
- This includes an "export" functionality that's a custom function you can implement per browser type
|
||||
- This includes a new field in `responses` for `custom_browser`
|
||||
- Added `SendMythicRPCCallbackTokenSearch`
|
||||
- Added `SendMythicRPCCustomBrowserSearch`
|
||||
- Added `verifier_regex` option to CommandParameters
|
||||
- Allows you to provide regex that must match the supplied value
|
||||
- Changed `mythictree` to move `has_children` out of metadata in support of CustomBrowsers
|
||||
- Changed `mythictree` to have new `display_path` field separate from `full_path` in support of CustomBrowsers
|
||||
- Added field to `payload` to track the version of the backing payload_type when the payload was created
|
||||
- This makes it easier to track payloads as you're doing payload type development/updates
|
||||
- Added `supported_c2` and `supported_wrapping` to Payload Types to track supported C2/Wrappers
|
||||
- This is now used to "resync" data rather than asking the container to resync back to Mythic
|
||||
- Changed `token_id` to int64 (from uint64) to match similar change in Postgres database to BigInt from just Integer
|
||||
- Updated operation event log auto resolver to use new `warining=true` vs `level='warning'` check
|
||||
- Added a 5s sleep between container's OnStart messages to help with multiple operations bombarding a container in rapid succession
|
||||
- Updated standard RPC timeouts to 10s instead of 5s
|
||||
- Updated auto re-hosting of files to have a 10s delay between files
|
||||
- Updated ContainerVersion out of bounds message to be a bit clearer
|
||||
- Updated `SendMythicRPCCallbackUpdate` to allow specifying the `dead` attribute
|
||||
- Updated `SendMythicRPCFileSearch` to allow specifying a PayloadUUID in addition to AgentFileID in `AgentFileID` field
|
||||
- Updated `SendMythicRPCResponseCreate` to go through the same process as an agent's `user_output`
|
||||
- This allows responses in this way to be intercepted and emitted to logging
|
||||
- Added some mutexes when syncing PayloadTypes to prevent possible race conditions
|
||||
- Updated file downloads to allow specifying a negative `total_chunks` until you're done and know how many chunks total there are
|
||||
- Added new `database` tab to file previews to run SQL queries on database right in the browser
|
||||
- Added the following new options for BuildParameter's HideConditionOperator
|
||||
- LessThan, GreaterThan, LessThanOrEqual, GreaterThanOrEqual, StartsWith, EndsWith, Contains, NotContains
|
||||
- [UI Updates](/MythicReactUI/CHANGELOG.MD#0383---2025-12-10)
|
||||
- [CLI Updates](/Mythic_CLI/Changelog.md#0321---2025-12-10)
|
||||
|
||||
## [3.4.10] - 2025-10-31
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -130,4 +130,49 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
SOFTWARE.
|
||||
|
||||
MIT license
|
||||
===========
|
||||
|
||||
Copyright (c) 2017 sql.js authors (see AUTHORS)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
|
||||
# Some portions of the Makefile taken from:
|
||||
Copyright 2017 Ryusei Yamaguchi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -4,6 +4,233 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.3.113] - 2026-07-01
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a bug in the process browser that would set a filter to undefined instead of ""
|
||||
|
||||
## [0.3.112] - 2026-05-17
|
||||
|
||||
- Updated the single task view to show interactive tasking surrounding results
|
||||
|
||||
## [0.3.111] - 2026-05-07
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue with useLazyQuery spamming requests
|
||||
|
||||
## [0.3.110] - 2026-04-16
|
||||
|
||||
### Changed
|
||||
|
||||
- Added more generic MythicPageBody and MythicServerHeader components to wrap most pages/headers
|
||||
- Added a new border color you can select for borders of a few components
|
||||
- Updated a few places with hard-coded px values to more scalable rem values
|
||||
|
||||
## [0.3.109] - 2026-04-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the UI to sort c2 profile parameters during payload build by ui_position if specified
|
||||
|
||||
## [0.3.107] - 2026-03-23
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed typo in file browser for remove_folder
|
||||
|
||||
## [0.3.106] - 2026-03-21
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue with clicking file uploads in modals
|
||||
|
||||
## [0.3.105] - 2026-03-20
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated some of the file upload pieces in the UI to support drag-and-drop
|
||||
- Added an `autoScroll` button to interactive tasking so you can force auto scroll or not
|
||||
- Fixed some of the drag and drop modal components
|
||||
|
||||
## [0.3.104] - 2026-03-19
|
||||
|
||||
### Changed
|
||||
|
||||
- Added ability to drag and move some modals and copy data from behind them
|
||||
|
||||
## [0.3.103] - 2026-03-17
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed issue with file uploads for payload/wrapper builds when starting from existing payload
|
||||
|
||||
## [0.3.102] - 2026-03-12
|
||||
|
||||
### Changed
|
||||
|
||||
- Slight tweak to `tabs` browser script capability to cap the max size of the tabs
|
||||
- Fixed a scrolling issue on the single task view page
|
||||
|
||||
## [0.3.101] - 2026-02-19
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue with database view in split view tasking that would cause overflow
|
||||
|
||||
## [0.3.100] - 2026-02-18
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the eventing page to only stream in updates that match the current filter
|
||||
- Updated payload parameter array type to start empty
|
||||
|
||||
## [0.3.99] - 2026-02-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated process_browser and file_browser tables to allow column sorting, hiding, and saved to user preferences
|
||||
- Updated process_browser to have a new column for integrity_level
|
||||
|
||||
## [0.3.98] - 2026-02-08
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the default shortcuts to remove Reporting and add CreatePayload
|
||||
- Updated Task Search on Parameters to include agent parameters, not just original and display
|
||||
- Updated callback table bulk actions to include a note about callback last checkin times not updates in those windows
|
||||
|
||||
## [0.3.97] - 2026-02-02
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated browser script tables to rever sort after toggling ASC/DESC to match other virtualized tables in Mythic
|
||||
|
||||
## [0.3.96] - 2026-01-30
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the file browser to support `file_browser:remove` and `file_browser:remove_folder`
|
||||
- this allows separate commands for deleting files vs deleting folders (or the same command if you want)
|
||||
|
||||
## [0.3.95] - 2026-01-29
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated tasking data for browser scripts to also provide callback's pid
|
||||
- Updated browser scripting to provide new type: `tabs`
|
||||
- returning `{"tabs": [ {"title": "tab title here", "content": {"table": {table dict} } } ] }`
|
||||
- this creates one tab with title `tab title here` that renders a table
|
||||
|
||||
## [0.3.94] - 2026-01-27
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a UI caching bug in split tasking view that would hide tasks on clicking previously issued tasks
|
||||
|
||||
## [0.3.93] - 2026-01-14
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated UI to allow output background color and output text color to be set separate from the "theme"
|
||||
- Updated UI to reflect file size updates from file browser while streaming
|
||||
|
||||
## [0.3.92] - 2026-01-09
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a bug in dynamic query parameters in the modal that would sometimes error on choices
|
||||
|
||||
## [0.3.91] - 2026-01-08
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a bug where c2 parameter deviations would try to iterate over null
|
||||
|
||||
## [0.3.90] - 2026-01-01
|
||||
|
||||
### Changed
|
||||
|
||||
- Added a new modal to the UI when looking at a payload's commands to get insight into how to task them via scripting
|
||||
|
||||
## [0.3.89] - 2025-12-31
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an alignment issue in the UI for split callbacks that would cause a horizontal scroll bar
|
||||
- Fixed a height issue on the browser script page that wasn't going the full page height
|
||||
- Added an option to string dialog boxes to wrap the displayed content or not
|
||||
|
||||
## [0.3.88] - 2025-12-29
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a bug where payload creation wouldn't properly show "False" settings from previously used configs
|
||||
- Fixed a few instances of double scrollbar in modals
|
||||
|
||||
## [0.3.87] - 2025-12-18
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the file media viewer to also fetch updates more dynamically
|
||||
|
||||
## [0.3.85] - 2025-12-15
|
||||
|
||||
### Changed
|
||||
|
||||
- This updates the default tasking view to split tasking
|
||||
- This adds the task to the top of the task output in the split tasking view for easier tracking
|
||||
|
||||
## [0.3.84] - 2025-12-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an ordering issues in backgrounds that got rid of the side bar colors in some browsers
|
||||
|
||||
## [0.3.83] - 2025-12-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated context menu for virtualized tables
|
||||
- Updated column adjustment icon for virtualized tables to be || instead of three vertical dots
|
||||
- Updated virtualized table header context menu display type
|
||||
- Updated minimum column width for virtualized columns to be 100 instead of 20
|
||||
- Updated virtualized tree views to check item.has_children instead of item.metadata.has_children
|
||||
- this is in support of custom browsers
|
||||
- Updated context menu for callbacks to display custom browser support
|
||||
- Updated callback tabs to have customFileBasedBrowser type for new custom browser support
|
||||
- Added custom browser table and tree views
|
||||
- Updated file browser to come more inline with custom browsers
|
||||
- Updated context menu for virtualized tree to have some copy options
|
||||
- Fixed an issue with `\\` in tasking on the command line when not surrounded by quotes
|
||||
- Updated token select in browser views to not be so squished
|
||||
- Updated split tasking view to align content
|
||||
- Added new `database` view for file previews with sql.js and wasm
|
||||
- Updated `icon` usage in browser scripts to support just supplying the font awesome name instead of a curated list
|
||||
- https://fontawesome.com/v6/search?s=solid&ic=free-collection all of these should be available
|
||||
- Updated task display to allow showing of operator names that bypassed OPSEC checks
|
||||
- Updated task buttons for split view to be along the right-hand side instead of a floating speeddial button at the bottom
|
||||
- Added verifier_regex to command parameters
|
||||
- Fixed a UI bug where display names of command parameter descriptions/names would overflow
|
||||
- Added support for broader range of hide conditions in payload type build parameters
|
||||
- Fixed a UI bug where payload selection options for wrappers weren't all sorted by time
|
||||
- Updated Eventing Instance Table view to be more in line with other virtualized tables
|
||||
- Updated view to show "selected" eventing workflow and task in split view tasking to be easier to see
|
||||
- Updated detailed payload views to show parameter names and descriptions, just like the building views
|
||||
- Updated hovering over payloads to show payload type semver when payload was created vs currently installed version
|
||||
- Added CustomBrowser entry to the "Installed Services" page
|
||||
- Added CustomBrowser search page
|
||||
- Fixed Task/Callback links on Token search page
|
||||
- Updated UI Configuration settings to support `showOPSECBypassUsername` and new `navBarBottomColor`
|
||||
- the latter allows you to have gradients for the side navbar instead of just a solid color
|
||||
|
||||
## [0.3.82] - 2025-11-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue with creating payloads without including any C2 profiles
|
||||
|
||||
## [0.3.81] - 2025-10-29
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
const webpack = require('webpack');
|
||||
module.exports = function override(config, env) {
|
||||
config.resolve.fallback = {
|
||||
//url: require.resolve('url'),
|
||||
fs: false,
|
||||
assert: require.resolve('assert'),
|
||||
crypto: require.resolve('crypto-browserify'),
|
||||
path: require.resolve('path-browserify'),
|
||||
//http: require.resolve('stream-http'),
|
||||
//https: require.resolve('https-browserify'),
|
||||
//os: require.resolve('os-browserify/browser'),
|
||||
//buffer: require.resolve('buffer'),
|
||||
stream: require.resolve('stream-browserify'),
|
||||
};
|
||||
/*
|
||||
config.plugins.push(
|
||||
new webpack.ProvidePlugin({
|
||||
process: 'process/browser',
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
);
|
||||
|
||||
*/
|
||||
|
||||
return config;
|
||||
}
|
||||
Generated
+923
-277
File diff suppressed because it is too large.
Load diff
@@ -6,7 +6,11 @@
|
||||
"author": "@its_a_feature_",
|
||||
"description": "React UI for Mythic C2 Framework",
|
||||
"devDependencies": {
|
||||
"react-scripts": "^5.0.1"
|
||||
"buffer": "^6.0.3",
|
||||
"crypto-browserify": "^3.12.1",
|
||||
"react-app-rewired": "^2.2.1",
|
||||
"react-scripts": "^5.0.1",
|
||||
"stream-browserify": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.13.8",
|
||||
@@ -38,6 +42,7 @@
|
||||
"jwt-decode": "^4.0.0",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"path-browserify": "^1.0.1",
|
||||
"randexp": "^0.5.3",
|
||||
"react": "^18.3.1",
|
||||
"react-ace": "^14.0.1",
|
||||
@@ -45,7 +50,7 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-moment": "^1.1.3",
|
||||
"react-router-dom": "^7.5.2",
|
||||
"react-router-dom": "^7.13.1",
|
||||
"react-scrollbar-size": "^5.0.0",
|
||||
"react-split": "^2.0.14",
|
||||
"react-toastify": "^11.0.5",
|
||||
@@ -55,18 +60,19 @@
|
||||
"react-window": "^1.8.11",
|
||||
"save-svg-as-png": "^1.4.17",
|
||||
"semver": "^7.6.2",
|
||||
"sql.js": "^1.13.0",
|
||||
"styled-components": "^6.1.17",
|
||||
"uuid": "^9.0.1",
|
||||
"webpack": "^5.99.6",
|
||||
"webpack": "^5.105.0",
|
||||
"word-wrap": "^1.2.5"
|
||||
},
|
||||
"scripts": {
|
||||
"react-start": "CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=true WDS_POLLING=true BROWSER=none PORT=3000 WDS_SOCKET_PORT=7443 WDS_SOCKET_PATH=ws react-scripts start",
|
||||
"react-build": "react-scripts build",
|
||||
"react-test": "react-scripts test",
|
||||
"react-start": "CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=true WDS_POLLING=true BROWSER=none PORT=3000 WDS_SOCKET_PORT=7443 WDS_SOCKET_PATH=ws react-app-rewired start",
|
||||
"react-build": "react-app-rewired build",
|
||||
"react-test": "react-app-rewired test",
|
||||
"react-eject": "react-scripts eject",
|
||||
"build": "npm run react-build",
|
||||
"start": "cross-env BROWSER=none CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=true WDS_POLLING=true PORT=3000 WDS_SOCKET_PORT=7443 WDS_SOCKET_PATH=ws npm run react-start"
|
||||
"start": "BROWSER=none CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=true WDS_POLLING=true PORT=3000 WDS_SOCKET_PORT=7443 WDS_SOCKET_PATH=ws npm run react-start"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
|
||||
@@ -19,6 +19,11 @@ export const taskTimestampDisplayFieldOptions = [
|
||||
}
|
||||
]
|
||||
export const taskingContextFieldsOptions = ["impersonation_context", "cwd", "user", "host", "ip", "pid", "process_short_name", "extra_info", "architecture"].sort();
|
||||
export const defaultShortcuts = [
|
||||
"ActiveCallbacks", "Payloads", "PayloadTypesAndC2",
|
||||
"Operations", "SearchFiles", "SearchProxies",
|
||||
"CreatePayload", "Eventing",
|
||||
].sort();
|
||||
export const operatorSettingDefaults = {
|
||||
fontSize: 12,
|
||||
navBarOpen: false,
|
||||
@@ -27,9 +32,10 @@ export const operatorSettingDefaults = {
|
||||
hideUsernames: false,
|
||||
showIP: false,
|
||||
showHostname: false,
|
||||
showOPSECBypassUsername: false,
|
||||
showCallbackGroups: false,
|
||||
useDisplayParamsForCLIHistory: true,
|
||||
interactType: "interact",
|
||||
interactType: "interactSplit",
|
||||
taskTimestampDisplayField: "timestamp",
|
||||
callbacks_table_columns: ["Interact", "Host", "Domain", "User", "Description", "Last Checkin", "Agent", "IP", "PID"],
|
||||
callbacks_table_filters: {},
|
||||
@@ -37,7 +43,8 @@ export const operatorSettingDefaults = {
|
||||
hideBrowserTasking: false,
|
||||
hideTaskingContext: false,
|
||||
taskingContextFields: ["impersonation_context", "cwd"],
|
||||
["experiment-responseStreamLimit"]: 50,
|
||||
["experiment-responseStreamLimit"]: 200,
|
||||
sideShortcuts: defaultShortcuts,
|
||||
palette: {
|
||||
primary: {
|
||||
dark: "#75859b",
|
||||
@@ -108,8 +115,12 @@ export const operatorSettingDefaults = {
|
||||
light: '#ffffff'
|
||||
},
|
||||
navBarColor: {
|
||||
dark: "#2e373c",
|
||||
light: "#3c4d67",
|
||||
dark: "#194573",
|
||||
light: "#3b606d",
|
||||
},
|
||||
navBarBottomColor: {
|
||||
dark: "#330814",
|
||||
light: "#283581",
|
||||
},
|
||||
taskPromptTextColor: {
|
||||
dark: '#bebebe',
|
||||
@@ -131,13 +142,25 @@ export const operatorSettingDefaults = {
|
||||
dark: "#2a5953",
|
||||
light: "#a7ce9d",
|
||||
},
|
||||
emptyFolderColor: {
|
||||
dark: '#bebebe',
|
||||
light: '#a6a5a5'
|
||||
},
|
||||
outputBackgroundColor: {
|
||||
dark: '#282828',
|
||||
light: '#f6f6f6'
|
||||
},
|
||||
outputTextColor: {
|
||||
dark: '#f6f6f6',
|
||||
light: '#282828',
|
||||
},
|
||||
borderColor: {
|
||||
dark: "#595858",
|
||||
light: "#c5c5c5"
|
||||
}
|
||||
},
|
||||
}
|
||||
export const defaultShortcuts = [
|
||||
"ActiveCallbacks", "Payloads", "PayloadTypesAndC2",
|
||||
"Operations", "SearchFiles", "SearchProxies",
|
||||
"Reporting", "Eventing",
|
||||
].sort();
|
||||
|
||||
export const mePreferences = makeVar(operatorSettingDefaults);
|
||||
|
||||
|
||||
|
||||
+141
-106
@@ -38,6 +38,18 @@ import {Eventing} from "./pages/Eventing/Eventing";
|
||||
import {InviteForm} from "./pages/Login/InviteForm";
|
||||
import {snackActions} from "./utilities/Snackbar";
|
||||
import {TopAppBarVertical} from "./TopAppBarVertical";
|
||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||
import * as Icons from '@fortawesome/free-solid-svg-icons';
|
||||
import {Jupyter} from "./pages/Jupyter/Jupyter";
|
||||
import {Hasura} from "./pages/Hasura/Hasura";
|
||||
|
||||
// add all fas icons
|
||||
const iconList = Object
|
||||
.keys(Icons)
|
||||
.filter(key => key !== "fas" && key !== "prefix" )
|
||||
.map(icon => Icons[icon])
|
||||
|
||||
library.add(...iconList)
|
||||
|
||||
export const MeContext = createContext({});
|
||||
export const userSettingsQuery = gql`
|
||||
@@ -130,8 +142,10 @@ export function App(props) {
|
||||
pageHeaderText: {
|
||||
main: 'white',
|
||||
},
|
||||
topAppBarColor: themeMode === 'dark' ? preferences?.palette?.navBarColor?.dark || operatorSettingDefaults.palette.navBarColor.dark :
|
||||
preferences?.palette?.navBarColor?.light || operatorSettingDefaults.palette.navBarColor.light,
|
||||
topAppBarColor: themeMode === 'dark' ? (preferences?.palette?.navBarColor?.dark || operatorSettingDefaults.palette.navBarColor.dark) :
|
||||
(preferences?.palette?.navBarColor?.light || operatorSettingDefaults.palette.navBarColor.light),
|
||||
topAppBarBottomColor: themeMode === 'dark' ? preferences?.palette?.navBarBottomColor?.dark || operatorSettingDefaults.palette.navBarBottomColor.dark :
|
||||
preferences?.palette?.navBarBottomColor?.light || operatorSettingDefaults.palette.navBarBottomColor.light,
|
||||
typography: {
|
||||
fontSize: 12, //preferences?.fontSize,
|
||||
fontFamily: preferences?.fontFamily
|
||||
@@ -146,6 +160,14 @@ export function App(props) {
|
||||
preferences?.palette?.taskContextImpersonationColor?.light || operatorSettingDefaults.palette.taskContextImpersonationColor.light,
|
||||
taskContextExtraColor: themeMode === 'dark' ? preferences?.palette?.taskContextExtraColor?.dark || operatorSettingDefaults.palette.taskContextExtraColor.dark :
|
||||
preferences?.palette?.taskContextExtraColor?.light || operatorSettingDefaults.palette.taskContextExtraColor.light,
|
||||
emptyFolderColor: themeMode === 'dark' ? preferences?.palette?.emptyFolderColor?.dark || operatorSettingDefaults.palette.emptyFolderColor.dark :
|
||||
preferences?.palette?.emptyFolderColor?.light || operatorSettingDefaults.palette.emptyFolderColor.light,
|
||||
outputBackgroundColor: themeMode === 'dark' ? preferences?.palette?.outputBackgroundColor?.dark || operatorSettingDefaults.palette.outputBackgroundColor.dark :
|
||||
preferences?.palette?.outputBackgroundColor?.light || operatorSettingDefaults.palette.outputBackgroundColor.light,
|
||||
outputTextColor: themeMode === 'dark' ? preferences?.palette?.outputTextColor?.dark || operatorSettingDefaults.palette.outputTextColor.dark :
|
||||
preferences?.palette?.outputTextColor?.light || operatorSettingDefaults.palette.outputTextColor.light,
|
||||
borderColor: themeMode === 'dark' ? preferences?.palette?.borderColor?.dark || operatorSettingDefaults.palette.borderColor.dark :
|
||||
preferences?.palette?.borderColor?.light || operatorSettingDefaults.palette.borderColor.light,
|
||||
})
|
||||
}catch(error){
|
||||
console.log(error);
|
||||
@@ -223,6 +245,16 @@ export function App(props) {
|
||||
},
|
||||
topAppBarColor: themeMode === 'dark' ? operatorSettingDefaults.palette.navBarColor.dark :
|
||||
operatorSettingDefaults.palette.navBarColor.light,
|
||||
topAppBarBottomColor: themeMode === 'dark' ? operatorSettingDefaults.palette.navBarBottomColor.dark :
|
||||
operatorSettingDefaults.palette.navBarBottomColor.light,
|
||||
emptyFolderColor: themeMode === 'dark' ? operatorSettingDefaults.palette.emptyFolderColor.dark :
|
||||
operatorSettingDefaults.palette.emptyFolderColor.light,
|
||||
outputBackgroundColor: themeMode === 'dark' ? operatorSettingDefaults.palette.outputBackgroundColor.dark :
|
||||
operatorSettingDefaults.palette.outputBackgroundColor.light,
|
||||
outputTextColor: themeMode === 'dark' ? operatorSettingDefaults.palette.outputTextColor.dark :
|
||||
operatorSettingDefaults.palette.outputTextColor.light,
|
||||
borderColor: themeMode === 'dark' ? operatorSettingDefaults.palette.borderColor.dark :
|
||||
operatorSettingDefaults.palette.borderColor.light,
|
||||
typography: {
|
||||
fontSize: 12,//operatorSettingDefaults?.fontSize,
|
||||
fontFamily: operatorSettingDefaults?.fontFamily
|
||||
@@ -305,113 +337,116 @@ export function App(props) {
|
||||
stacked={false}
|
||||
style={{maxWidth: "100%", minWidth: "40%", display: "flex", flexWrap: "wrap",
|
||||
wordBreak: "break-all", flexDirection: "column", justifyContent: "center",}}
|
||||
pauseOnFocusLoss={false} />
|
||||
<div style={{ maxHeight: '100%', height: '100%', display: 'flex', flexDirection: 'row', maxWidth: "100%", width:"100%",
|
||||
...background}}>
|
||||
|
||||
{openRefreshDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openRefreshDialog}
|
||||
onClose={()=>{setOpenRefreshDialog(false);}}
|
||||
innerDialog={<RefreshTokenDialog
|
||||
onClose={()=>{setOpenRefreshDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
{me.loggedIn && me.user !== undefined && me.user !== null ? (
|
||||
<TopAppBarVertical me={me} theme={themeMode} toggleTheme={themeToggler} />
|
||||
) : null}
|
||||
<div style={{
|
||||
maxHeight: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: "100%"
|
||||
}}>
|
||||
{me.loggedIn && me?.user?.current_operation_banner_text !== "" &&
|
||||
<Typography style={{
|
||||
backgroundColor: me?.user?.current_operation_banner_color,
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
fontWeight: "600",
|
||||
color: "white",
|
||||
border: `1px solid ${theme.topAppBarColor || "grey"}`
|
||||
}}>
|
||||
{me?.user?.current_operation_banner_text}
|
||||
</Typography>
|
||||
}
|
||||
{me.loggedIn && me?.badConnection
|
||||
&&
|
||||
<Typography style={{
|
||||
backgroundColor: theme.palette.error.main,
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
fontWeight: "600",
|
||||
color: "white",
|
||||
border: `1px solid ${theme.topAppBarColor || "grey"}`
|
||||
}}>
|
||||
{"Can't connect to Mythic. Please check connection and refresh"}
|
||||
</Typography>
|
||||
}
|
||||
<div style={{
|
||||
margin: '0px 0px 0px 0px',
|
||||
flexGrow: 1,
|
||||
overflowY: "hidden",
|
||||
flexDirection: 'column',
|
||||
height: "100%",
|
||||
pauseOnFocusLoss={false}
|
||||
/>
|
||||
<div style={{ maxHeight: '100%', height: '100%', display: 'flex', flexDirection: 'row', maxWidth: "100%", width:"100%",
|
||||
...background}}>
|
||||
{openRefreshDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openRefreshDialog}
|
||||
onClose={()=>{setOpenRefreshDialog(false);}}
|
||||
innerDialog={<RefreshTokenDialog
|
||||
onClose={()=>{setOpenRefreshDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
{me.loggedIn && me.user !== undefined && me.user !== null &&
|
||||
<TopAppBarVertical me={me} toggleTheme={themeToggler} />
|
||||
}
|
||||
<div style={{
|
||||
maxHeight: '100%',
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<div style={{height: "5px", width: "100%", background: me.loggedIn ? `linear-gradient(25deg, ${theme.topAppBarColor}, ${theme.topAppBarBottomColor})` : ""}}/>
|
||||
{me.loggedIn && me?.user?.current_operation_banner_text !== "" &&
|
||||
<Typography style={{
|
||||
backgroundColor: me?.user?.current_operation_banner_color,
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
fontWeight: "600",
|
||||
color: "white",
|
||||
border: `1px solid ${theme.topAppBarColor || "grey"}`
|
||||
}}>
|
||||
<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>}/>
|
||||
<Route exact path='/new/payloadtypes'
|
||||
element={<LoggedInRoute me={me}><PayloadTypesC2Profiles
|
||||
me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/eventfeed'
|
||||
element={<LoggedInRoute me={me}><EventFeed me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/createpayload'
|
||||
element={<LoggedInRoute me={me}><CreatePayload me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/createwrapper'
|
||||
element={<LoggedInRoute me={me}><CreatePayloadWrapper
|
||||
me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/payloads'
|
||||
element={<LoggedInRoute me={me}><Payloads me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/c2profiles'
|
||||
element={<LoggedInRoute me={me}><PayloadTypesC2Profiles
|
||||
me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/services/'
|
||||
element={<LoggedInRoute me={me}><PayloadTypesC2Profiles
|
||||
me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/callbacks'
|
||||
element={<LoggedInRoute me={me}><Callbacks me={me}/></LoggedInRoute>}/>
|
||||
<Route path='/new/search'
|
||||
element={<LoggedInRoute me={me}><Search history={props.history}
|
||||
me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/browserscripts'
|
||||
element={<LoggedInRoute me={me}><BrowserScripts me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/task/:taskId'
|
||||
element={<LoggedInRoute me={me}><SingleTaskView me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/tasks/by_range'
|
||||
element={<LoggedInRoute me={me}><SingleTaskView me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/operations'
|
||||
element={<LoggedInRoute me={me}><Operations me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/callbacks/:callbackDisplayId'
|
||||
element={<LoggedInRoute me={me}><ExpandedCallback
|
||||
me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/reporting'
|
||||
element={<LoggedInRoute me={me}><Reporting me={me}/></LoggedInRoute>}/>
|
||||
<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/eventing'
|
||||
element={<LoggedInRoute me={me}><Eventing me={me}/></LoggedInRoute>}/>
|
||||
</Routes>
|
||||
</div>
|
||||
{me?.user?.current_operation_banner_text}
|
||||
</Typography>
|
||||
}
|
||||
{me.loggedIn && me?.badConnection
|
||||
&&
|
||||
<Typography style={{
|
||||
backgroundColor: theme.palette.error.main,
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
fontWeight: "600",
|
||||
color: "white",
|
||||
border: `1px solid ${theme.topAppBarColor || "grey"}`
|
||||
}}>
|
||||
{"Can't connect to Mythic. Please check connection and refresh"}
|
||||
</Typography>
|
||||
}
|
||||
<div style={{
|
||||
margin: '0px 0px 0px 0px',
|
||||
flexGrow: 1,
|
||||
display: "flex",
|
||||
flexDirection: 'column',
|
||||
overflow:"hidden"
|
||||
}}>
|
||||
<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>}/>
|
||||
<Route exact path='/new/payloadtypes'
|
||||
element={<LoggedInRoute me={me}><PayloadTypesC2Profiles
|
||||
me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/eventfeed'
|
||||
element={<LoggedInRoute me={me}><EventFeed me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/createpayload'
|
||||
element={<LoggedInRoute me={me}><CreatePayload me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/createwrapper'
|
||||
element={<LoggedInRoute me={me}><CreatePayloadWrapper
|
||||
me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/payloads'
|
||||
element={<LoggedInRoute me={me}><Payloads me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/c2profiles'
|
||||
element={<LoggedInRoute me={me}><PayloadTypesC2Profiles
|
||||
me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/services/'
|
||||
element={<LoggedInRoute me={me}><PayloadTypesC2Profiles
|
||||
me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/callbacks'
|
||||
element={<LoggedInRoute me={me}><Callbacks me={me}/></LoggedInRoute>}/>
|
||||
<Route path='/new/search'
|
||||
element={<LoggedInRoute me={me}><Search history={props.history}
|
||||
me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/browserscripts'
|
||||
element={<LoggedInRoute me={me}><BrowserScripts me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/task/:taskId'
|
||||
element={<LoggedInRoute me={me}><SingleTaskView me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/tasks/by_range'
|
||||
element={<LoggedInRoute me={me}><SingleTaskView me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/operations'
|
||||
element={<LoggedInRoute me={me}><Operations me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/callbacks/:callbackDisplayId'
|
||||
element={<LoggedInRoute me={me}><ExpandedCallback
|
||||
me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/reporting'
|
||||
element={<LoggedInRoute me={me}><Reporting me={me}/></LoggedInRoute>}/>
|
||||
<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/eventing'
|
||||
element={<LoggedInRoute me={me}><Eventing me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/jupyter' element={<LoggedInRoute me={me}><Jupyter/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/hasura' element={<LoggedInRoute me={me}><Hasura/></LoggedInRoute>}/>
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MeContext.Provider>
|
||||
</ThemeProvider>
|
||||
</StyledEngineProvider>
|
||||
|
||||
@@ -27,14 +27,15 @@ export const MythicAgentSVGIcon = ({payload_type, style, is_p2p}) => {
|
||||
|
||||
)
|
||||
}
|
||||
export const MythicAgentSVGIconNoTooltip = ({payload_type, is_p2p, className}) => {
|
||||
export const MythicAgentSVGIconNoTooltip = ({payload_type, is_p2p, className, style}) => {
|
||||
const theme = useTheme();
|
||||
if(payload_type === ""){
|
||||
return;
|
||||
}
|
||||
const extraStyles = style ? style : {};
|
||||
return (
|
||||
<div style={{display: "inline-block", left: "50%"}} className={className}>
|
||||
<img src={"/static/" + payload_type + "_" + theme.palette.mode + ".svg"} style={{height: "20px", margin: 'auto'}}/>
|
||||
<img src={"/static/" + payload_type + "_" + theme.palette.mode + ".svg"} style={{height: "20px", margin: 'auto', ...extraStyles}}/>
|
||||
{is_p2p === false &&
|
||||
<WifiIcon style={{width: "10px", height: "10px", marginLeft: "-5px", marginBottom: "0px",
|
||||
borderRadius: "10px", border: "1px solid",
|
||||
|
||||
@@ -18,10 +18,29 @@ 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';
|
||||
import WrapTextIcon from '@mui/icons-material/WrapText';
|
||||
import {IconButton} from '@mui/material';
|
||||
import {MythicStyledTooltip} from "./MythicStyledTooltip";
|
||||
import Draggable from 'react-draggable';
|
||||
import {MythicDraggableDialogTitle} from "./MythicDraggableDialogTitle";
|
||||
|
||||
export function MythicDialog(props) {
|
||||
const descriptionElementRef = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
const [draggedState, setDraggedState] = React.useState({
|
||||
style: {},
|
||||
paperStyle: {
|
||||
margin: "0",
|
||||
height: "fit-content",
|
||||
width: "stretch"
|
||||
},
|
||||
containerStyle: {
|
||||
|
||||
},
|
||||
hideBackdrop: false,
|
||||
modified: false
|
||||
});
|
||||
const nodeRef = React.useRef(null);
|
||||
const descriptionElementRef = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
if (props.open) {
|
||||
const { current: descriptionElement } = descriptionElementRef;
|
||||
if (descriptionElement !== null) {
|
||||
@@ -30,35 +49,97 @@ export function MythicDialog(props) {
|
||||
}
|
||||
}, [props.open]);
|
||||
const dialogOnClick = (e) => {
|
||||
e.stopPropagation();
|
||||
//e.preventDefault();
|
||||
//e.stopPropagation();
|
||||
if(e.target.classList.length > 0 && e.target.classList.contains("MuiDialog-container")){
|
||||
props.onClose();
|
||||
props.onClose();
|
||||
}
|
||||
}
|
||||
const dialogOnContextMenu = (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
}
|
||||
const handleOnClose = (event, reason) => {
|
||||
if(reason === "backdropClick" && draggedState.hideBackdrop){
|
||||
return;
|
||||
}
|
||||
props.onClose();
|
||||
}
|
||||
const onStart = (e) => {
|
||||
if(e){
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
if(!draggedState.modified){
|
||||
setDraggedState({
|
||||
style: {
|
||||
height: e.target.offsetParent.offsetHeight + "px",
|
||||
width: e.target.offsetParent.offsetWidth + "px",
|
||||
margin: "auto",
|
||||
overflowY: "auto",
|
||||
},
|
||||
paperStyle: {
|
||||
//height: e.target.offsetParent.offsetHeight + "px",
|
||||
width: e.target.offsetParent.offsetWidth + "px",
|
||||
margin: 0,
|
||||
overflowY: "auto",
|
||||
},
|
||||
containerStyle: {
|
||||
height: "fit-content",
|
||||
overflowY: "auto"
|
||||
},
|
||||
hideBackdrop: true,
|
||||
modified: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
const onStop = (e) => {
|
||||
if(e){
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Dialog
|
||||
open={props.open}
|
||||
onClose={props.onClose}
|
||||
scroll="paper"
|
||||
maxWidth={props.maxWidth}
|
||||
fullWidth={props.fullWidth}
|
||||
style={props.style}
|
||||
aria-labelledby="scroll-dialog-title"
|
||||
aria-describedby="scroll-dialog-description"
|
||||
onMouseDown={dialogOnClick}
|
||||
onContextMenu={dialogOnContextMenu}
|
||||
<Draggable
|
||||
nodeRef={nodeRef}
|
||||
handle="#mythic-draggable-title"
|
||||
cancel={'[class*="MuiDialogContent-root"]'}
|
||||
onStart={onStart}
|
||||
onStop={onStop}
|
||||
>
|
||||
{props.innerDialog}
|
||||
</Dialog>
|
||||
<Dialog
|
||||
ref={nodeRef}
|
||||
open={props.open}
|
||||
onClick={dialogOnClick}
|
||||
onClose={handleOnClose}
|
||||
scroll="paper"
|
||||
maxWidth={props.maxWidth}
|
||||
fullWidth={true}
|
||||
style={{...props.style, ...draggedState.style}}
|
||||
disableEnforceFocus={true}
|
||||
disablePortal={false}
|
||||
hideBackdrop={draggedState.hideBackdrop}
|
||||
aria-labelledby="scroll-dialog-title"
|
||||
aria-describedby="scroll-dialog-description"
|
||||
sx={{
|
||||
[".MuiPaper-root"]: {
|
||||
...draggedState.paperStyle
|
||||
},
|
||||
[".MuiDialog-container"]: {
|
||||
...draggedState.containerStyle
|
||||
}
|
||||
}}
|
||||
onMouseDown={dialogOnClick}
|
||||
onContextMenu={dialogOnContextMenu}
|
||||
>
|
||||
{props.innerDialog}
|
||||
</Dialog>
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
export function MythicModifyStringDialog(props) {
|
||||
const [comment, setComment] = React.useState("");
|
||||
const [wrap, setWrap] = React.useState(props.wrap ? props.wrap : false);
|
||||
const theme = useTheme();
|
||||
const onCommitSubmit = () => {
|
||||
props.onSubmit(comment);
|
||||
@@ -80,37 +161,48 @@ export function MythicModifyStringDialog(props) {
|
||||
}, [props.value]);
|
||||
return (
|
||||
<React.Fragment>
|
||||
{props.title !== "" && <DialogTitle id="form-dialog-title">{props.title}</DialogTitle>}
|
||||
<DialogContent dividers={true} style={{height: "100%", margin: 0, padding: 0}}>
|
||||
{props.title !== "" &&
|
||||
<MythicDraggableDialogTitle>{props.title}
|
||||
<MythicStyledTooltip title={wrap ? "Toggle off word wrap" : "Toggl on word wrap"}
|
||||
tooltipStyle={{float: "right"}}>
|
||||
<IconButton onClick={() => {setWrap(!wrap)}}>
|
||||
<WrapTextIcon color={wrap ? "success" : "secondary"} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</MythicDraggableDialogTitle>
|
||||
}
|
||||
<DialogContent dividers={true} style={{ margin: 0, padding: 0}}>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
|
||||
width="100%"
|
||||
height="100%"
|
||||
fontSize={14}
|
||||
showPrintMargin={false}
|
||||
wrapEnabled={props.wrap ? props.wrap : false}
|
||||
minLines={props.maxRows ? props.maxRows : 10}
|
||||
maxLines={props.maxRows ? props.maxRows : 10}
|
||||
wrapEnabled={wrap}
|
||||
value={comment}
|
||||
focus={true}
|
||||
onChange={onChange}
|
||||
setOptions={{
|
||||
useWorker: false
|
||||
tabSize: 4,
|
||||
useWorker: false,
|
||||
showInvisibles: false,
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{props.onClose &&
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
Close
|
||||
</Button>
|
||||
}
|
||||
{props.onSubmit &&
|
||||
<Button onClick={onCommitSubmit} variant="contained" color="success">
|
||||
{props.onSubmitText ? props.onSubmitText : "Submit"}
|
||||
</Button>
|
||||
}
|
||||
</DialogActions>
|
||||
{ (props.onClose || props.onSubmit) &&
|
||||
<DialogActions>
|
||||
{props.onClose &&
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
Close
|
||||
</Button>
|
||||
}
|
||||
{props.onSubmit &&
|
||||
<Button onClick={onCommitSubmit} variant="contained" color="success">
|
||||
{props.onSubmitText ? props.onSubmitText : "Submit"}
|
||||
</Button>
|
||||
}
|
||||
</DialogActions>
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -182,7 +274,7 @@ export function MythicViewJSONAsTableDialog(props) {
|
||||
}, [props.value, props.leftColumn, props.rightColumn]);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title" style={{wordBreak: "break-all", maxWidth: "100%"}}>{props.title}</DialogTitle>
|
||||
<MythicDraggableDialogTitle style={{wordBreak: "break-all", maxWidth: "100%"}}>{props.title}</MythicDraggableDialogTitle>
|
||||
|
||||
<TableContainer className="mythicElement" style={{paddingLeft: "10px"}}>
|
||||
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
|
||||
|
||||
export const MythicDraggableDialogTitle = ({children}) => {
|
||||
return (
|
||||
<DialogTitle id="mythic-draggable-title" style={{ cursor: 'move', width: "100%" }}>
|
||||
{children}
|
||||
</DialogTitle>
|
||||
)
|
||||
}
|
||||
@@ -239,7 +239,7 @@ const VirtualTreeRow = React.memo(({
|
||||
height: "15px",
|
||||
marginLeft: '3px',
|
||||
marginRight: '5px',
|
||||
color: item?.metadata?.has_children ? theme.folderColor : theme.palette.text.secondary,
|
||||
color: item?.has_children || item.success ? theme.folderColor : theme.emptyFolderColor,
|
||||
}}
|
||||
size={"lg"}
|
||||
onClick={handleOnClickButton} />
|
||||
@@ -250,14 +250,14 @@ const VirtualTreeRow = React.memo(({
|
||||
height: "15px",
|
||||
marginLeft: '3px',
|
||||
marginRight: '5px',
|
||||
color: item?.metadata?.has_children ? theme.folderColor : theme.palette.text.secondary, }}
|
||||
color: item?.has_children || item.success ? theme.folderColor : theme.emptyFolderColor, }}
|
||||
size={"lg"}
|
||||
icon={faFolder} onClick={handleOnClickButton} />
|
||||
)}
|
||||
<Typography
|
||||
style={{
|
||||
color:
|
||||
item?.metadata?.has_children ||
|
||||
item?.has_children ||
|
||||
item.success !== null
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary,
|
||||
|
||||
@@ -71,6 +71,13 @@ export const MythicFileContext = ({agent_file_id, display_link, filename, extraS
|
||||
if(fileData.agent_file_id === '' || fileData.filename === undefined){
|
||||
return agent_file_id
|
||||
}
|
||||
const onClose = (e) => {
|
||||
if(e){
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
setOpenPreviewMediaDialog(false)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<MythicStyledTooltip title={"Preview Media"} tooltipStyle={extraStyles ? extraStyles : {}}>
|
||||
@@ -83,11 +90,11 @@ export const MythicFileContext = ({agent_file_id, display_link, filename, extraS
|
||||
</Link>
|
||||
{openPreviewMediaDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openPreviewMediaDialog}
|
||||
onClose={(e)=>{setOpenPreviewMediaDialog(false);}}
|
||||
onClose={onClose}
|
||||
innerDialog={<PreviewFileMediaDialog
|
||||
agent_file_id={fileData.agent_file_id}
|
||||
filename={fileData.filename}
|
||||
onClose={(e)=>{setOpenPreviewMediaDialog(false);}} />}
|
||||
onClose={onClose} />}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
export const MythicPageBody = ({children}) => {
|
||||
return (
|
||||
<div style={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
margin: "0.5rem",
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {Typography} from "@mui/material";
|
||||
|
||||
export const MythicPageHeader = ({children, title, style={}, headerVariant="h5"}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Paper elevation={2}
|
||||
variant={"elevation"}
|
||||
style={{
|
||||
backgroundColor: theme.pageHeader.main,
|
||||
color: theme.pageHeaderText.main,
|
||||
border: `1px solid ${theme.borderColor}`,
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: "0.5rem",
|
||||
padding: "0.5rem",
|
||||
borderRadius: "5px",
|
||||
...style
|
||||
}}
|
||||
>
|
||||
<Typography variant={headerVariant} style={{textAlign: "left", display: "inline-block", marginLeft: "1rem", width: "100%"}}>
|
||||
{title}
|
||||
</Typography>
|
||||
<div style={{display: "flex", marginRight: "1rem", alignItems: "center"}}>
|
||||
{children}
|
||||
</div>
|
||||
</Paper>
|
||||
)
|
||||
|
||||
}
|
||||
@@ -22,6 +22,8 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
[data, rowIndex]
|
||||
);
|
||||
const handleClick = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if(data.onRowClick){
|
||||
data.onRowClick({event, rowDataStatic: data.items[rowIndex][columnIndex]?.props?.rowData})
|
||||
}
|
||||
@@ -44,6 +46,8 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
}
|
||||
}
|
||||
const handleMenuItemClick = (event, clickOption) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
clickOption({event, columnIndex, rowIndex, data: data.items[rowIndex][columnIndex]?.props?.rowData || {}});
|
||||
setOpenContextMenu(false);
|
||||
};
|
||||
@@ -55,7 +59,8 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
const handleContextClick = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
if(item.disableFilterMenu){
|
||||
event.stopPropagation();
|
||||
if(item?.disableFilterMenu){
|
||||
return;
|
||||
}
|
||||
contextMenuLocationRef.current.x = event.clientX;
|
||||
@@ -90,14 +95,14 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
{item}
|
||||
</div>
|
||||
<ContextMenu dropdownAnchorRef={dropdownAnchorRef} contextMenuOptions={contextMenuOptions}
|
||||
disableFilterMenu={item.disableFilterMenu} openContextMenu={openContextMenu}
|
||||
disableFilterMenu={item?.disableFilterMenu} openContextMenu={openContextMenu}
|
||||
contextMenuLocationRef={contextMenuLocationRef}
|
||||
setOpenContextMenu={setOpenContextMenu} handleMenuItemClick={handleMenuItemClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const ContextMenu = ({openContextMenu, dropdownAnchorRef, contextMenuOptions, setOpenContextMenu, handleMenuItemClick, contextMenuLocationRef}) => {
|
||||
export const ContextMenu = ({openContextMenu, dropdownAnchorRef, contextMenuOptions, setOpenContextMenu, handleMenuItemClick, contextMenuLocationRef}) => {
|
||||
const handleClose = (event) => {
|
||||
//if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
|
||||
// return;
|
||||
|
||||
+4
-2
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
||||
import {classes} from './styles';
|
||||
import Draggable from 'react-draggable';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import DragHandleIcon from '@mui/icons-material/DragHandle';
|
||||
|
||||
const DraggableHandles = React.forwardRef(({ height, rowHeight, width, minColumnWidth, columnWidths, onStop }, ref) => {
|
||||
const [isDragging, setIsDragging] = useState(-1);
|
||||
@@ -40,13 +41,14 @@ const DraggableHandles = React.forwardRef(({ height, rowHeight, width, minColumn
|
||||
setIsDragging(-1);
|
||||
onStop(data.x, i);
|
||||
}}>
|
||||
<MoreVertIcon
|
||||
<DragHandleIcon
|
||||
ref={nodeRef}
|
||||
className={isDragging === i ? classes.draggableHandlesClickAreaSelected : classes.draggableHandlesClickArea}
|
||||
style={{
|
||||
left: leftOffset + columnWidths[i] - 1 - 7,
|
||||
rotate: isDragging === i ? "0deg" : "90deg",
|
||||
}}>
|
||||
</MoreVertIcon>
|
||||
</DragHandleIcon>
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -5,13 +5,7 @@ import {classes} from './styles';
|
||||
import React from 'react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faFilter} from '@fortawesome/free-solid-svg-icons';
|
||||
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';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import {ContextMenu} from "./Cell";
|
||||
|
||||
const HeaderCell = ({
|
||||
onClick = () => {},
|
||||
@@ -23,9 +17,9 @@ const HeaderCell = ({
|
||||
VariableSizeGridProps: { style, rowIndex, columnIndex, data },
|
||||
}) => {
|
||||
const dropdownAnchorRef = React.useRef(null);
|
||||
const theme = useTheme();
|
||||
const item = data.items[rowIndex][columnIndex];
|
||||
const isFiltered = item?.filtered || false;
|
||||
const contextMenuLocationRef = React.useRef({x: 0, y: 0});
|
||||
const handleClick = useCallback(
|
||||
(e) => {
|
||||
onClick(e, columnIndex);
|
||||
@@ -47,22 +41,20 @@ const HeaderCell = ({
|
||||
return;
|
||||
}
|
||||
if(contextMenuOptions && contextMenuOptions.length > 0){
|
||||
|
||||
contextMenuLocationRef.current.x = event.clientX;
|
||||
contextMenuLocationRef.current.y = event.clientY;
|
||||
setOpenContextMenu(true);
|
||||
}
|
||||
},
|
||||
[contextMenuOptions, columnIndex] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
);
|
||||
const handleMenuItemClick = (event, index) => {
|
||||
contextMenuOptions[index].click({event, columnIndex});
|
||||
const handleMenuItemClick = (event, clickOption) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
clickOption({event, columnIndex, rowIndex, data: data.items[rowIndex][columnIndex]?.props?.rowData || {}});
|
||||
setOpenContextMenu(false);
|
||||
};
|
||||
const handleClose = (event) => {
|
||||
if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
setOpenContextMenu(false);
|
||||
};
|
||||
|
||||
const handleClicks = useSingleAndDoubleClick(handleClick, handleDoubleClick);
|
||||
|
||||
return (
|
||||
@@ -73,31 +65,11 @@ const HeaderCell = ({
|
||||
</Typography>
|
||||
{isFiltered && <FontAwesomeIcon icon={faFilter} />}
|
||||
{sortIndicatorIndex === columnIndex && (sortDirection === 'ASC' ? <div>↑</div> : <div>↓</div>)}
|
||||
<Popper open={openContextMenu} anchorEl={dropdownAnchorRef.current} role={undefined} transition style={{zIndex: 40}}>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
|
||||
}}
|
||||
>
|
||||
<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) => (
|
||||
<MenuItem
|
||||
key={option.name + index}
|
||||
onClick={(event) => handleMenuItemClick(event, index)}
|
||||
>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
<ContextMenu dropdownAnchorRef={dropdownAnchorRef} contextMenuOptions={contextMenuOptions}
|
||||
disableFilterMenu={item?.disableFilterMenu} openContextMenu={openContextMenu}
|
||||
contextMenuLocationRef={contextMenuLocationRef}
|
||||
setOpenContextMenu={setOpenContextMenu} handleMenuItemClick={handleMenuItemClick}
|
||||
/>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
|
||||
+5
-2
@@ -11,7 +11,7 @@ import {GetMythicSetting, useSetMythicSetting} from "../MythicSavedUserSetting";
|
||||
|
||||
const HeaderCellContext = createContext({});
|
||||
|
||||
const MIN_COLUMN_WIDTH = 20;
|
||||
const MIN_COLUMN_WIDTH = 100;
|
||||
const MIN_FLEX_COLUMN_WIDTH = 150;
|
||||
|
||||
|
||||
@@ -195,7 +195,10 @@ const ResizableGridWrapper = ({
|
||||
return itemRow[columnIndex]?.props?.cellData.length;
|
||||
}
|
||||
if(columns[columnIndex].type === "size"){
|
||||
if(itemRow[columnIndex]?.props?.cellData){
|
||||
if(itemRow[columnIndex]?.props?.cellData !== undefined){
|
||||
if(typeof itemRow[columnIndex]?.props?.cellData === 'number'){
|
||||
return String(itemRow[columnIndex]?.props?.cellData)?.length;
|
||||
}
|
||||
return String(itemRow[columnIndex]?.props?.cellData?.plaintext)?.length;
|
||||
}
|
||||
return itemRow[columnIndex].length;
|
||||
|
||||
@@ -39,7 +39,7 @@ export function MythicTabPanel(props) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function a11yProps(index) {
|
||||
export function a11yProps(index) {
|
||||
return {
|
||||
id: `scrollable-auto-tab-${index}`,
|
||||
'aria-controls': `scrollable-auto-tabpanel-${index}`,
|
||||
|
||||
@@ -29,6 +29,7 @@ import Typography from '@mui/material/Typography';
|
||||
import MythicStyledTableCell from "./MythicTableCell";
|
||||
import {meState} from "../../cache";
|
||||
import { useReactiveVar } from '@apollo/client';
|
||||
import {MythicDraggableDialogTitle} from "./MythicDraggableDialogTitle";
|
||||
|
||||
const createNewTagMutationTemplate = ({target_object}) => {
|
||||
// target_object should be something like "task_id"
|
||||
@@ -495,20 +496,17 @@ const onAcceptDelete = () => {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Edit Tags
|
||||
</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
{openNewDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="xl" open={openNewDialog}
|
||||
<MythicDraggableDialogTitle >Edit Tags </MythicDraggableDialogTitle>
|
||||
<DialogContent dividers={true} style={{width: "100%"}}>
|
||||
{openNewDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openNewDialog}
|
||||
onClose={()=>{setOpenNewDialog(false);}}
|
||||
innerDialog={<NewTagDialog me={props.me}
|
||||
target_object={props.target_object}
|
||||
target_object_id={props.target_object_id}
|
||||
onClose={()=>{setOpenNewDialog(false);}}
|
||||
onSubmit={handleNewTagCreate} />}
|
||||
/>) : null}
|
||||
<TableContainer className="mythicElement">
|
||||
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
/>}
|
||||
<Table size="small" style={{ width: "100%", overflow: "scroll"}}>
|
||||
<TableBody>
|
||||
<TableRow hover>
|
||||
<MythicStyledTableCell style={{width: "30%"}}>Select Existing Tag to Edit or Add New</MythicStyledTableCell>
|
||||
@@ -578,7 +576,6 @@ return (
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
@@ -647,10 +644,9 @@ export function NewTagDialog(props) {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Add New Tag</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
<TableContainer className="mythicElement">
|
||||
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
<MythicDraggableDialogTitle >Add New Tag</MythicDraggableDialogTitle>
|
||||
<DialogContent dividers={true} style={{width: "100%"}}>
|
||||
<Table size="small" style={{ overflow: "scroll", width: "100%"}}>
|
||||
<TableBody>
|
||||
<TableRow hover>
|
||||
<MythicStyledTableCell style={{width: "20%"}}>
|
||||
@@ -717,7 +713,6 @@ export function NewTagDialog(props) {
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
|
||||
@@ -78,6 +78,8 @@ import {
|
||||
} from "@hello-pangea/dnd";
|
||||
import {reorder} from "./MythicComponents/MythicDraggableList";
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import LogoutIcon from '@mui/icons-material/Logout';
|
||||
import TuneIcon from '@mui/icons-material/Tune';
|
||||
|
||||
const PREFIX = 'TopAppBarVertical';
|
||||
|
||||
@@ -90,12 +92,14 @@ const openedMixin = (theme) => ({
|
||||
overflowX: 'hidden',
|
||||
borderRadius: "0 !important",
|
||||
border: "0px !important",
|
||||
backgroundColor: "unset !important",
|
||||
});
|
||||
const closedMixin = (theme) => ({
|
||||
overflowX: 'hidden',
|
||||
width: `calc(${theme.spacing(4)} + 1px)`,
|
||||
borderRadius: "0 !important",
|
||||
border: "0px !important",
|
||||
border: "0px !important",
|
||||
backgroundColor: "unset !important",
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
width: `calc(${theme.spacing(5)} + 1px)`,
|
||||
borderRadius: "0 !important",
|
||||
@@ -108,28 +112,44 @@ const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open'
|
||||
flexShrink: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
boxSizing: 'border-box',
|
||||
background: `linear-gradient(180deg, ${theme.topAppBarColor}, ${theme.topAppBarBottomColor}) !important`,
|
||||
backgroundColor: `unset !important`,
|
||||
[`& .${classes.listSubHeader}`]: {
|
||||
//color: ` ${theme.navBarTextIconColor} !important`,
|
||||
backgroundColor: `${theme.topAppBarColor} !important`,
|
||||
//backgroundColor: `${theme.topAppBarColor} !important`,
|
||||
backgroundColor: "unset !important",
|
||||
},
|
||||
[`& .${classes.listSubHeader}:hover`]: {
|
||||
backgroundColor: `${theme.topAppBarColor} !important`,
|
||||
//backgroundColor: `${theme.topAppBarColor} !important`,
|
||||
filter: `brightness(90%)`,
|
||||
color: `${theme.navBarTextIconColor} !important`,
|
||||
backgroundColor: "unset !important",
|
||||
},
|
||||
variants: [
|
||||
{
|
||||
props: ({ open }) => open,
|
||||
style: {
|
||||
...openedMixin(theme),
|
||||
'& .MuiDrawer-paper': openedMixin(theme),
|
||||
'& .MuiDrawer-paper': {
|
||||
...openedMixin(theme),
|
||||
backgroundColor: "unset !important",
|
||||
'& .MuiList-root': {
|
||||
backgroundColor: "unset !important",
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
props: ({ open }) => !open,
|
||||
style: {
|
||||
...closedMixin(theme),
|
||||
'& .MuiDrawer-paper': closedMixin(theme),
|
||||
'& .MuiDrawer-paper': {
|
||||
...closedMixin(theme),
|
||||
backgroundColor: "unset !important",
|
||||
'& .MuiList-root': {
|
||||
backgroundColor: "unset !important",
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -143,6 +163,7 @@ export const StyledListItem = styled(ListItem)(
|
||||
marginTop: 0,
|
||||
paddingBottom: "2px",
|
||||
color: theme.navBarTextColor,
|
||||
backgroundColor: "unset !important",
|
||||
}),
|
||||
);
|
||||
export const StyledListItemIcon = styled(ListItemIcon)(
|
||||
@@ -152,6 +173,7 @@ export const StyledListItemIcon = styled(ListItemIcon)(
|
||||
paddingBottom: 0,
|
||||
minWidth: "45px",
|
||||
//color: theme.navBarTextIconColor,
|
||||
backgroundColor: "unset !important",
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -416,7 +438,7 @@ const Eventing = () => {
|
||||
}
|
||||
const JupyterNotebook = () => {
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} target="_blank" component={Link} to='/jupyter' key={"jupyter"} >
|
||||
<StyledListItem className={classes.listSubHeader} target="_blank" component={Link} to='/jupyter' key={"jupyter"} >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Jupyter Notebooks"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<img src={JupyterLogo} height={"25px"} width={"25px"} />
|
||||
@@ -428,7 +450,7 @@ const JupyterNotebook = () => {
|
||||
}
|
||||
const GraphQL = () => {
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} target="_blank" component={Link} to='/console' key={"console"} >
|
||||
<StyledListItem className={classes.listSubHeader} target="_blank" component={Link} to='/console' key={"console"} >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"GraphQL Console"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<img src={GraphQLLogo} height={"25px"} width={"25px"} className="mythicElement"/>
|
||||
@@ -736,9 +758,10 @@ export function TopAppBarVertical(props) {
|
||||
<>
|
||||
{me?.user?.current_operation_id ? (<EventFeedNotifications me={me} />) : null }
|
||||
<Drawer anchor="left" variant="permanent" open={menuOpen} onClose={handleDrawerClose}
|
||||
style={{borderRight: "1px solid grey !important"}}>
|
||||
style={{borderRight: "1px solid grey !important", backgroundColor: "unset !important"
|
||||
}}>
|
||||
<List style={{paddingTop: 0, marginTop: 0, height: "100%", display: "flex", flexDirection: "column",
|
||||
backgroundColor: theme.topAppBarColor,
|
||||
backgroundColor: "unset !important",
|
||||
borderBottom: "unset !important", borderLeft: "unset !important", borderTop: "unset !important"}}>
|
||||
<ListItem className={classes.listSubHeader} style={{marginTop:0, paddingTop: 0, paddingLeft: "2px", paddingBottom: 0}}>
|
||||
<ListItemIcon >
|
||||
@@ -877,16 +900,29 @@ function TopBarRightShortcutsVertical({me, menuOpen, serverName}){
|
||||
{
|
||||
name: (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
<Typography variant={"caption"} style={{marginBottom: "0"}}>Signed in as:</Typography>
|
||||
<Typography variant={"body1"} style={{marginBottom: "0", fontWeight: 600}}>{me?.user?.username}</Typography>
|
||||
</>),
|
||||
disabled: true,
|
||||
click: handleSettingsClose
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div style={{display: "flex", alignItems: "center"}}>
|
||||
<TuneIcon style={{marginRight: "0.5rem"}}/>
|
||||
<Typography variant="body1" style={{marginBottom: "0", }}> Settings </Typography>
|
||||
</div>
|
||||
),
|
||||
to: "/new/settings",
|
||||
component: Link,
|
||||
click: handleSettingsClose
|
||||
},
|
||||
{
|
||||
name: "Logout",
|
||||
name: (
|
||||
<div style={{display: "flex", alignItems: "center"}}>
|
||||
<LogoutIcon style={{marginRight: "0.5rem"}} /> Logout
|
||||
</div>
|
||||
),
|
||||
click: handleLogout,
|
||||
to: "",
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {BrowserScriptsTable} from './BrowserScriptsTable';
|
||||
import {snackActions} from '../../utilities/Snackbar';
|
||||
import { Backdrop } from '@mui/material';
|
||||
import {CircularProgress} from '@mui/material';
|
||||
import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
|
||||
|
||||
|
||||
const SUB_BrowserScripts = gql`
|
||||
@@ -135,7 +136,7 @@ export function BrowserScripts({me}){
|
||||
}
|
||||
}, [])
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicPageBody>
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={false}>
|
||||
<CircularProgress color="inherit" />
|
||||
</Backdrop>
|
||||
@@ -145,6 +146,6 @@ export function BrowserScripts({me}){
|
||||
onSubmitEdit={onSubmitEdit} onRevert={onRevert} onSubmitNew={onSubmitCreateNewBrowserScript}
|
||||
/>
|
||||
|
||||
</React.Fragment>
|
||||
</MythicPageBody>
|
||||
);
|
||||
}
|
||||
@@ -13,31 +13,28 @@ import AddCircleIcon from '@mui/icons-material/AddCircle';
|
||||
import Button from '@mui/material/Button';
|
||||
import { MythicDialog } from '../../MythicComponents/MythicDialog';
|
||||
import {EditScriptDialog} from './EditScriptDialog';
|
||||
import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
|
||||
|
||||
|
||||
export function BrowserScriptsTable(props){
|
||||
const theme = useTheme();
|
||||
const [openNewScriptDialog, setOpenNewScriptDialog] = React.useState(false);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main}} variant={"elevation"}>
|
||||
<Typography variant="h5" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
|
||||
Browser Scripts
|
||||
</Typography>
|
||||
<Button size="small" onClick={() => setOpenNewScriptDialog(true)} style={{float: "right", marginRight: "30px", color: "white"}}
|
||||
<>
|
||||
<MythicPageHeader title={"Browser Scripts"}>
|
||||
<Button size="small" onClick={() => setOpenNewScriptDialog(true)} style={{whiteSpace: "nowrap", color: "white"}}
|
||||
startIcon={<AddCircleIcon color={"success"} style={{backgroundColor: "white", borderRadius: "10px"}}/>} >
|
||||
New Script
|
||||
</Button>
|
||||
{openNewScriptDialog ? (
|
||||
{openNewScriptDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openNewScriptDialog}
|
||||
onClose={()=>{setOpenNewScriptDialog(false);}}
|
||||
innerDialog={
|
||||
<EditScriptDialog me={props.me} onClose={()=>{setOpenNewScriptDialog(false);}} title="Create New Browser Script" new={true} onSubmitEdit={props.onSubmitNew} />
|
||||
} />
|
||||
) : null
|
||||
} />
|
||||
}
|
||||
</Paper>
|
||||
<TableContainer className="mythicElement" style={{maxHeight: "90%"}}>
|
||||
</MythicPageHeader>
|
||||
<TableContainer className="mythicElement" style={{height: "100%", flexGrow: 1}}>
|
||||
<Table stickyHeader={true} size="small" style={{"tableLayout": "fixed", "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
@@ -61,8 +58,8 @@ export function BrowserScriptsTable(props){
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</React.Fragment>
|
||||
</TableContainer>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import CardHeader from '@mui/material/CardHeader';
|
||||
import {gql, useQuery} from '@apollo/client';
|
||||
import { CardContent } from '@mui/material';
|
||||
import {classes, StyledButton, StyledDivider} from '../../MythicComponents/MythicTransferList';
|
||||
import {MythicDraggableDialogTitle} from "../../MythicComponents/MythicDraggableDialogTitle";
|
||||
|
||||
const getCommandsQuery = gql`
|
||||
query GetCallbackDetails($callback_id: Int!) {
|
||||
@@ -184,8 +185,8 @@ export function AddRemoveCallbackCommandsDialog(props) {
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Add or Remove Commands for Callback {props.display_id} </DialogTitle>
|
||||
<DialogContent dividers={true} style={{height: "100%", display: "flex", flexDirection: "column", position: "relative", maxHeight: "100%"}}>
|
||||
<DialogTitle>Add or Remove Commands for Callback {props.display_id} </DialogTitle>
|
||||
<DialogContent dividers={true} style={{height: "100%", display: "flex", flexDirection: "column", position: "relative", overflowY: "auto"}}>
|
||||
This will add or remove commands associated with this callback from Mythic's perspective.
|
||||
This does NOT add or remove commands within the payload itself that's beaconing out to Mythic.
|
||||
<div style={{display: "flex", flexDirection: "row", overflowY: "auto", flexGrow: 1, minHeight: 0}}>
|
||||
|
||||
@@ -106,6 +106,7 @@ export const taskingDataFragment = gql`
|
||||
domain
|
||||
ip
|
||||
mythictree_groups
|
||||
pid
|
||||
}
|
||||
callback_id
|
||||
commentOperator{
|
||||
@@ -146,8 +147,16 @@ export const taskingDataFragment = gql`
|
||||
command_name
|
||||
opsec_pre_blocked
|
||||
opsec_pre_bypassed
|
||||
opsec_pre_bypass_user {
|
||||
id
|
||||
username
|
||||
}
|
||||
opsec_post_blocked
|
||||
opsec_post_bypassed
|
||||
opsec_post_bypass_user {
|
||||
id
|
||||
username
|
||||
}
|
||||
is_interactive_task
|
||||
interactive_task_type
|
||||
has_intercepted_response
|
||||
@@ -167,6 +176,7 @@ export const taskingDataFragment = gql`
|
||||
domain
|
||||
ip
|
||||
mythictree_groups
|
||||
pid
|
||||
}
|
||||
completed
|
||||
subtask_group_name
|
||||
|
||||
@@ -19,8 +19,12 @@ export const getCallbackIdFromClickedTab = (tabId) => {
|
||||
return Number(tabId.split("fileBrowser")[0]);
|
||||
}else if(tabId.includes("interact")){
|
||||
return Number(tabId.split("interact")[0]);
|
||||
}else if(tabId.includes("processBrowser")){
|
||||
}else if(tabId.includes("processBrowser")) {
|
||||
return Number(tabId.split("processBrowser")[0]);
|
||||
}else if(tabId.includes("customFileBasedBrowser")) {
|
||||
return Number(tabId.split("customFileBasedBrowser")[0]);
|
||||
}else if(tabId.includes("customProcessBasedBrowser")) {
|
||||
return Number(tabId.split("customProcessBasedBrowser")[0]);
|
||||
} else {
|
||||
console.log("unknown tab type", tabId);
|
||||
return 0;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useMemo, useContext} from 'react';
|
||||
import {MythicDialog} from '../../MythicComponents/MythicDialog';
|
||||
import {MythicDialog, MythicModifyStringDialog} from '../../MythicComponents/MythicDialog';
|
||||
import {
|
||||
exportCallbackConfigQuery,
|
||||
hideCallbackMutation, lockCallbackMutation, unlockCallbackMutation, updateCallbackTriggerMutation,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
updateSleepInfoCallbackMutation
|
||||
} from './CallbackMutations';
|
||||
import {snackActions} from '../../utilities/Snackbar';
|
||||
import {useMutation, useLazyQuery } from '@apollo/client';
|
||||
import {useMutation, useLazyQuery, gql } from '@apollo/client';
|
||||
import {
|
||||
CallbacksTableIDCell,
|
||||
CallbacksTableStringCell,
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
CallbacksTableIPCell, CallbacksTableTagsCell
|
||||
} from './CallbacksTableRow';
|
||||
import MythicResizableGrid from '../../MythicComponents/MythicResizableGrid';
|
||||
import {TableFilterDialog} from './TableFilterDialog';
|
||||
import {CallbacksTabsHideMultipleDialog, CallbacksTabsSelectMultipleDialog} from "./CallbacksTabsHideMultipleDialog";
|
||||
import {CallbacksTabsTaskMultipleDialog} from "./CallbacksTabsTaskMultipleDialog";
|
||||
import ip6 from 'ip6';
|
||||
@@ -56,7 +55,26 @@ import {CallbacksTableColumnsReorderDialog} from "./CallbacksTableColumnsReorder
|
||||
import PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwoTone';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import {EventTriggerContextSelectDialog} from "../Eventing/EventTriggerContextSelect";
|
||||
import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery";
|
||||
import {faFilter} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export const getCustomBrowsersQuery = gql(`
|
||||
query getCustomBrowsersQuery{
|
||||
custombrowser(where: {deleted: {_eq: false}}, order_by: {name: asc}){
|
||||
id
|
||||
name
|
||||
type
|
||||
separator
|
||||
columns
|
||||
default_visible_columns
|
||||
indicate_partial_listing
|
||||
show_current_path
|
||||
extra_table_inputs
|
||||
row_actions
|
||||
export_function
|
||||
}
|
||||
}
|
||||
`);
|
||||
export const ipCompare = (a, b) => {
|
||||
let aJSON = JSON.parse(a);
|
||||
if(aJSON.length === 0){return 0}
|
||||
@@ -298,13 +316,13 @@ function CallbacksTablePreMemo(props){
|
||||
const ele = document.getElementById("download_config");
|
||||
if(ele !== null){
|
||||
ele.href = URL.createObjectURL(dataBlob);
|
||||
ele.download = data.exportCallbackConfig.agent_callback_id + ".json";
|
||||
ele.download = "exported_callback.json";
|
||||
ele.click();
|
||||
}else{
|
||||
const element = document.createElement("a");
|
||||
element.id = "download_config";
|
||||
element.href = URL.createObjectURL(dataBlob);
|
||||
element.download = data.exportCallbackConfig.agent_callback_id + ".json";
|
||||
element.download = "exported_callback.json";
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
}
|
||||
@@ -316,7 +334,8 @@ function CallbacksTablePreMemo(props){
|
||||
console.log(data);
|
||||
snackActions.error("Failed to export configuration: " + data.message)
|
||||
}
|
||||
})
|
||||
});
|
||||
const getCustomBrowsers = useMythicLazyQuery(getCustomBrowsersQuery, {fetchPolicy: "no-cache"});
|
||||
const taskingData = React.useRef({"parameters": "", "ui_feature": "callback_table:exit"});
|
||||
const [openTaskingButton, setOpenTaskingButton] = React.useState(false);
|
||||
const eventingDataRef = React.useRef({});
|
||||
@@ -327,7 +346,10 @@ function CallbacksTablePreMemo(props){
|
||||
updateTrigger({variables: {callback_display_id: openTriggerDialog.display_id, trigger_on_checkin_after_time: newTriggerValue}})
|
||||
setOpenTriggerDialog({...openTriggerDialog, open: false});
|
||||
}
|
||||
const onRowContextClick = ({rowDataStatic}) => {
|
||||
async function getCustomBrowsersForContextMenu(){
|
||||
return await getCustomBrowsers({}).then(result => {return result.data?.custombrowser});
|
||||
}
|
||||
async function onRowContextClick({rowDataStatic}) {
|
||||
// based on row, return updated options array?
|
||||
let defaultInteractIcon = <KeyboardIcon style={{paddingRight: "5px"}}/>;
|
||||
if(interactType === "interactSplit"){
|
||||
@@ -335,6 +357,26 @@ function CallbacksTablePreMemo(props){
|
||||
} else if(interactType === "interactConsole"){
|
||||
defaultInteractIcon = <TerminalIcon style={{paddingRight: "5px"}}/>;
|
||||
}
|
||||
let customBrowsers = await getCustomBrowsersForContextMenu();
|
||||
customBrowsers = customBrowsers.map(b => {
|
||||
return {
|
||||
name: b.name,
|
||||
icon: b.type === 'file' ? <FontAwesomeIcon icon={faFolderOpen} style={{color: theme.folderColor, cursor: "pointer", marginRight: "10px"}} /> : <AccountTreeIcon style={{paddingRight: "5px"}}/>,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const tabType = b.type === 'file' ? 'customFileBasedBrowser' : 'customProcessBasedBrowser';
|
||||
onOpenTab({
|
||||
tabType: tabType,
|
||||
tabID: rowDataStatic.id + tabType + b.name,
|
||||
callbackID: rowDataStatic.id,
|
||||
color: rowDataStatic.color,
|
||||
displayID: rowDataStatic.display_id,
|
||||
customBrowser: b
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
return [
|
||||
{
|
||||
name: <div style={{display: "flex", flexDirection: "column"}}>
|
||||
@@ -433,6 +475,7 @@ function CallbacksTablePreMemo(props){
|
||||
{
|
||||
name: 'File Browser', icon: <FontAwesomeIcon icon={faFolderOpen} style={{color: theme.folderColor, cursor: "pointer", marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const tabType = "fileBrowser";
|
||||
onOpenTab({
|
||||
@@ -446,6 +489,7 @@ function CallbacksTablePreMemo(props){
|
||||
{
|
||||
name: 'Process Browser', icon: <AccountTreeIcon style={{paddingRight: "5px"}}/>,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const tabType = "processBrowser";
|
||||
onOpenTab({
|
||||
@@ -456,6 +500,17 @@ function CallbacksTablePreMemo(props){
|
||||
displayID: rowDataStatic.display_id});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: <div style={{display: "flex", flexDirection: "column", width: "100%"}}>
|
||||
<Typography>
|
||||
{"Custom Agent Browsers:"}
|
||||
</Typography>
|
||||
</div>,
|
||||
icon: null, click: ({event}) => {},
|
||||
type: "item",
|
||||
disabled: true
|
||||
},
|
||||
...customBrowsers
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -583,10 +638,10 @@ function CallbacksTablePreMemo(props){
|
||||
}
|
||||
];
|
||||
}
|
||||
const callbackDropdown = ({rowDataStatic, event}) => {
|
||||
callbackDropdownRef.current.options = onRowContextClick({rowDataStatic});
|
||||
async function callbackDropdown({rowDataStatic, event}) {
|
||||
callbackDropdownRef.current.options = await onRowContextClick({rowDataStatic});
|
||||
callbackDropdownRef.current.callback = rowDataStatic;
|
||||
callbackDropdownRef.current.dropdownAnchorRef = event.currentTarget;
|
||||
callbackDropdownRef.current.dropdownAnchorRef = event?.currentTarget || event.target;
|
||||
setOpenCallbackDropdown(true);
|
||||
}
|
||||
React.useEffect( () => {
|
||||
@@ -656,16 +711,16 @@ function CallbacksTablePreMemo(props){
|
||||
return;
|
||||
}
|
||||
if (!column.key) {
|
||||
setSortData({"sortKey": null, "sortType":null, "sortDirection": "ASC"});
|
||||
setSortData({sortKey: null, sortType:null, sortDirection: "ASC"});
|
||||
}
|
||||
if (sortData.sortKey === column.key) {
|
||||
if (sortData.sortDirection === 'ASC') {
|
||||
setSortData({...sortData, "sortDirection": "DESC"});
|
||||
setSortData({...sortData, sortDirection: "DESC"});
|
||||
} else {
|
||||
setSortData({"sortKey": null, "sortType":null, "sortDirection": "ASC"});
|
||||
setSortData({sortKey: null, sortType:null, sortDirection: "ASC"});
|
||||
}
|
||||
} else {
|
||||
setSortData({"sortKey": column.key, "sortType":column.type, "sortDirection": "ASC"});
|
||||
setSortData({sortKey: column.key, sortType:column.type, sortDirection: "ASC"});
|
||||
}
|
||||
};
|
||||
const onRowDoubleClick = React.useCallback( (event, rowID, rowDataStatic) => {
|
||||
@@ -679,23 +734,28 @@ function CallbacksTablePreMemo(props){
|
||||
}, []);
|
||||
const contextMenuOptions = [
|
||||
{
|
||||
name: 'Filter Column',
|
||||
name: 'Filter Column', type: "item",
|
||||
icon: <FontAwesomeIcon icon={faFilter} style={{paddingRight: "5px"}} />,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
if(columns[columnIndex].disableFilterMenu){
|
||||
snackActions.warning("Can't filter that column");
|
||||
return;
|
||||
}
|
||||
setSelectedColumn(columns[columnIndex]);
|
||||
setOpenContextMenu(true);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Reorder Columns and Adjust Visibility",
|
||||
name: "Reorder Columns and Adjust Visibility", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
setOpenReorderDialog(true);
|
||||
}
|
||||
}
|
||||
@@ -882,10 +942,10 @@ function CallbacksTablePreMemo(props){
|
||||
}
|
||||
}, [])
|
||||
}, [callbacks, sortData, filterOptions, columnVisibility, clickedCallbackID]);
|
||||
const onSubmitFilterOptions = (newFilterOptions) => {
|
||||
setFilterOptions(newFilterOptions);
|
||||
const onSubmitFilterOptions = (value) => {
|
||||
setFilterOptions({...filterOptions, [selectedColumn.key]: value });
|
||||
try{
|
||||
updateSetting({setting_name: "callbacks_table_filter_options", value: newFilterOptions});
|
||||
updateSetting({setting_name: "callbacks_table_filter_options", value: {...filterOptions, [selectedColumn.key]: value }});
|
||||
}catch(error){
|
||||
console.log("failed to save filter options");
|
||||
}
|
||||
@@ -963,11 +1023,14 @@ function CallbacksTablePreMemo(props){
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={<TableFilterDialog
|
||||
selectedColumn={selectedColumn}
|
||||
filterOptions={filterOptions}
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
onClose={()=>{setOpenContextMenu(false);}} />}
|
||||
innerDialog={<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={filterOptions[selectedColumn.key]}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>}
|
||||
/>
|
||||
}
|
||||
{openEditDescriptionDialog &&
|
||||
|
||||
+4
-1
@@ -48,6 +48,7 @@ export function CallbacksTableEditDescriptionColorDialog(props) {
|
||||
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
|
||||
width="100%"
|
||||
height="100%"
|
||||
fontSize={14}
|
||||
wrapEnabled={props.wrap ? props.wrap : false}
|
||||
minLines={props.maxRows ? props.maxRows : 3}
|
||||
maxLines={props.maxRows ? props.maxRows : 3}
|
||||
@@ -55,7 +56,9 @@ export function CallbacksTableEditDescriptionColorDialog(props) {
|
||||
focus={true}
|
||||
onChange={onChange}
|
||||
setOptions={{
|
||||
useWorker: false
|
||||
tabSize: 4,
|
||||
useWorker: false,
|
||||
showInvisibles: false,
|
||||
}}
|
||||
/>
|
||||
<HexColorPicker color={color} onChange={setColor} style={{width: "100%"}} />
|
||||
|
||||
+13
-3
@@ -17,7 +17,11 @@ import MythicTextField from "../../MythicComponents/MythicTextField";
|
||||
export function CallbacksTableEditTriggerOnCheckinDialog(props) {
|
||||
const [comment, setComment] = React.useState(0);
|
||||
const onChange = (name, value, error) => {
|
||||
setComment(parseInt(value));
|
||||
if(isNaN(parseInt(value))){
|
||||
setComment(0);
|
||||
} else {
|
||||
setComment(parseInt(value));
|
||||
}
|
||||
}
|
||||
useEffect( () => {
|
||||
setComment(props.trigger_on_checkin_after_time);
|
||||
@@ -29,8 +33,14 @@ export function CallbacksTableEditTriggerOnCheckinDialog(props) {
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">{"Adjust this callback's trigger threshold"}</DialogTitle>
|
||||
<DialogContent dividers={true} style={{height: "100%"}}>
|
||||
<Typography>
|
||||
{"This adjusts how long, in minutes, this callback must not checkin before finally checking in to trigger an eventing workflow (trigger is callback_checkin). A zero value means never trigger."}
|
||||
<Typography >
|
||||
This adjusts how long, in minutes, this callback must <b>not</b> checkin before finally checking in to trigger an <b>eventing workflow</b> (trigger is callback_checkin).
|
||||
</Typography>
|
||||
<Typography >
|
||||
A zero value means never trigger.
|
||||
</Typography>
|
||||
<Typography >
|
||||
If no eventing workflow for <b>callback_checkin</b> that matches the right payload_types and supported_os restrictions, then nothing will happen.
|
||||
</Typography>
|
||||
<MythicTextField autoFocus={true} onChange={onChange} type={"number"} value={comment} onEnter={onSubmit} name={"trigger threshold in minutes"} showLabel={false} />
|
||||
</DialogContent>
|
||||
|
||||
@@ -10,6 +10,10 @@ import { CallbacksTabsFileBrowserLabel, CallbacksTabsFileBrowserPanel } from './
|
||||
import { CallbacksTabsProcessBrowserLabel, CallbacksTabsProcessBrowserPanel } from './CallbacksTabsProcessBrowser';
|
||||
import { CallbacksTabsTaskingSplitLabel, CallbacksTabsTaskingSplitPanel} from "./CallbacksTabsTaskingSplit";
|
||||
import {CallbacksTabsTaskingConsoleLabel, CallbacksTabsTaskingConsolePanel} from "./CallbacksTabsTaskingConsole";
|
||||
import {
|
||||
CallbacksTabsCustomFileBasedBrowserLabel,
|
||||
CallbacksTabsCustomFileBasedBrowserPanel
|
||||
} from "./CallbacksTabsCustomFileBasedBrowser";
|
||||
|
||||
export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clickedTabId, setClickedTabId, onEditTabDescription, contextMenuOptions, me, newDataForTab, setNewDataForTab}) {
|
||||
|
||||
@@ -134,12 +138,12 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clic
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
/>
|
||||
)
|
||||
case 'fileBrowser':
|
||||
case 'customFileBasedBrowser':
|
||||
return (
|
||||
<CallbacksTabsFileBrowserLabel
|
||||
<CallbacksTabsCustomFileBasedBrowserLabel
|
||||
onEditTabDescription={onEditTabDescription}
|
||||
onCloseTab={onCloseTabLocal}
|
||||
key={'tablabel' + tab.tabID + tab.tabType}
|
||||
key={'tablabel' + tab.tabID + tab.tabType + tab?.customBrowser?.name}
|
||||
tabInfo={tab}
|
||||
index={index}
|
||||
me={me}
|
||||
@@ -164,6 +168,21 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clic
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
/>
|
||||
);
|
||||
case 'fileBrowser':
|
||||
return (
|
||||
<CallbacksTabsFileBrowserLabel
|
||||
onEditTabDescription={onEditTabDescription}
|
||||
onCloseTab={onCloseTabLocal}
|
||||
key={'tablabel' + tab.tabID + tab.tabType}
|
||||
tabInfo={tab}
|
||||
index={index}
|
||||
me={me}
|
||||
newDataForTab={newDataForTab}
|
||||
selectedIndex={value}
|
||||
onDragTab={onDragTab}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -237,6 +256,25 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clic
|
||||
parentMountedRef={mountedRef}
|
||||
/>
|
||||
)
|
||||
case 'customFileBasedBrowser':
|
||||
return (
|
||||
<CallbacksTabsCustomFileBasedBrowserPanel
|
||||
style={{
|
||||
height: '100%',
|
||||
maxHeight: '100%',
|
||||
position: 'relative',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
onCloseTab={onCloseTabLocal}
|
||||
key={'tabpanel' + tab.tabID + tab.tabType + tab?.customBrowser?.name}
|
||||
tabInfo={tab}
|
||||
setNewDataForTab={setNewDataForTab}
|
||||
value={value}
|
||||
index={index}
|
||||
me={me}
|
||||
parentMountedRef={mountedRef}
|
||||
/>
|
||||
);
|
||||
case 'fileBrowser':
|
||||
return (
|
||||
<CallbacksTabsFileBrowserPanel
|
||||
@@ -254,6 +292,7 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clic
|
||||
index={index}
|
||||
me={me}
|
||||
parentMountedRef={mountedRef}
|
||||
baseUIFeature={"file_browser"}
|
||||
/>
|
||||
);
|
||||
case 'processBrowser':
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+976
@@ -0,0 +1,976 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useMutation, useLazyQuery, gql } from '@apollo/client';
|
||||
import { snackActions } from '../../utilities/Snackbar';
|
||||
import {
|
||||
MythicDialog,
|
||||
MythicModifyStringDialog,
|
||||
MythicViewJSONAsTableDialog,
|
||||
} from '../../MythicComponents/MythicDialog';
|
||||
import {faFolder} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import DescriptionIcon from '@mui/icons-material/Description';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import ErrorIcon from '@mui/icons-material/Error';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { IconButton } from '@mui/material';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import ListIcon from '@mui/icons-material/List';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import GetAppIcon from '@mui/icons-material/GetApp';
|
||||
import 'react-virtualized/styles.css';
|
||||
import FileCopyOutlinedIcon from '@mui/icons-material/FileCopyOutlined';
|
||||
import { copyStringToClipboard } from '../../utilities/Clipboard';
|
||||
import MythicResizableGrid from '../../MythicComponents/MythicResizableGrid';
|
||||
import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip';
|
||||
import {faFilter} from '@fortawesome/free-solid-svg-icons';
|
||||
import {MythicTransferListDialog} from '../../MythicComponents/MythicTransferList';
|
||||
import {TagsDisplay, ViewEditTags} from '../../MythicComponents/MythicTag';
|
||||
import { toLocalTime } from '../../utilities/Time';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import {RenderSingleTask} from "../SingleTaskView/SingleTaskView";
|
||||
import {
|
||||
GetComputedFontSize,
|
||||
GetMythicSetting,
|
||||
useSetMythicSetting
|
||||
} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {getIconColor, getIconName} from "./ResponseDisplayTable";
|
||||
import {CallbacksTableColumnsReorderDialog} from "./CallbacksTableColumnsReorderDialog";
|
||||
|
||||
const updateFileComment = gql`
|
||||
mutation updateCommentMutation($mythictree_id: Int!, $comment: String!) {
|
||||
update_mythictree_by_pk(pk_columns: { id: $mythictree_id }, _set: { comment: $comment }) {
|
||||
comment
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CallbacksTabsCustomFileBasedBrowserTable = (props) => {
|
||||
const theme = useTheme();
|
||||
const [updateSetting] = useSetMythicSetting();
|
||||
const [allData, setAllData] = React.useState([]);
|
||||
const [openReorderDialog, setOpenReorderDialog] = React.useState(false);
|
||||
const [openContextMenu, setOpenContextMenu] = React.useState(false);
|
||||
const [filterOptions, setFilterOptions] = React.useState({});
|
||||
const [selectedColumn, setSelectedColumn] = React.useState({});
|
||||
const [fileCommentDialogOpen, setFileCommentDialogOpen] = React.useState(false);
|
||||
const [viewPermissionsDialogOpen, setViewPermissionsDialogOpen] = React.useState(false);
|
||||
const permissionDataRef = React.useRef({metadata: {}, name: ""});
|
||||
const commentDataRef = React.useRef({id: 0, comment: ""});
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [sortData, setSortData] = React.useState({"sortKey": null, "sortDirection": null, "sortType": null})
|
||||
const [columnVisibility, setColumnVisibility] = React.useState({
|
||||
"visible": [...props.treeConfig.table.visible],
|
||||
"hidden": [...props.treeConfig.table.hidden]
|
||||
});
|
||||
const indicatePartialListingRef = React.useRef(props.treeConfig.indicate_partial_listing);
|
||||
const [selectedRows, setSelectedRows] = React.useState([]);
|
||||
const [updateComment] = useMutation(updateFileComment, {
|
||||
onCompleted: (data) => {
|
||||
snackActions.success('updated comment');
|
||||
},
|
||||
});
|
||||
const onSubmitUpdatedComment = (comment) => {
|
||||
updateComment({ variables: { mythictree_id: commentDataRef.current.id, comment: comment } });
|
||||
};
|
||||
const [initialDefaultColumns, setInitialDefaultColumns] = React.useState([
|
||||
{ name: 'name', type: 'name', key: 'name_text', fillWidth: true, visible: true },
|
||||
...props.treeConfig.table.columns,
|
||||
{ name: 'tags', type: 'tags', key: 'tags', disableSort: true, disableFilterMenu: true, width: 220 },
|
||||
{ name: 'comment', type: 'comment', key: 'comment', width: 200 },
|
||||
]);
|
||||
const [columnOrder, setColumnOrder] = React.useState(initialDefaultColumns);
|
||||
const columns = React.useMemo(
|
||||
() =>
|
||||
columnOrder.reduce( (prev, cur) => {
|
||||
if(columnVisibility.visible.includes(cur.name)){
|
||||
if(filterOptions[cur.key] && String(filterOptions[cur.key]).length > 0){
|
||||
return [...prev, {...cur, filtered: true}];
|
||||
}else{
|
||||
return [...prev, {...cur}];
|
||||
}
|
||||
}else{
|
||||
return [...prev];
|
||||
}
|
||||
}, [])
|
||||
, [filterOptions, columnVisibility, columnOrder]
|
||||
);
|
||||
|
||||
const sortedData = React.useMemo(() => {
|
||||
if (sortData.sortKey === null || sortData.sortType === null) {
|
||||
return allData;
|
||||
}
|
||||
const tempData = [...allData];
|
||||
if (sortData.sortType === 'number' || sortData.sortType === 'size' || sortData.sortType === 'date') {
|
||||
tempData.sort((a, b) => {
|
||||
if(sortData.inMetadata){
|
||||
try {
|
||||
if(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a]?.metadata[sortData.sortKey] ===
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b]?.metadata[sortData.sortKey]){
|
||||
return 0;
|
||||
}
|
||||
if (props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a]?.metadata[sortData.sortKey] === null ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a]?.metadata[sortData.sortKey] === undefined) {
|
||||
return -1;
|
||||
}
|
||||
if (props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b]?.metadata[sortData.sortKey] === null ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b]?.metadata[sortData.sortKey] === undefined) {
|
||||
return 1;
|
||||
}
|
||||
return parseInt(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a]?.metadata[sortData.sortKey]) >
|
||||
parseInt(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b]?.metadata[sortData.sortKey]) ? 1 : -1;
|
||||
}catch(error) {
|
||||
console.log("failed to parse data for sorting", error);
|
||||
return props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a]?.metadata[sortData.sortKey] >
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b]?.metadata[sortData.sortKey] ? 1 : -1
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
if(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] ===
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey]){
|
||||
return 0;
|
||||
}
|
||||
if (props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] === null ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] === undefined) {
|
||||
return -1;
|
||||
}
|
||||
if (props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey] === null ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey] === undefined) {
|
||||
return 1;
|
||||
}
|
||||
return parseInt(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey]) > parseInt(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey]) ? 1 : -1;
|
||||
} catch (error) {
|
||||
console.log("failed to parse data for sorting", error);
|
||||
return props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] > props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
} else if (sortData.sortType === 'string') {
|
||||
tempData.sort((a, b) => {
|
||||
try{
|
||||
if(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] ===
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey]){
|
||||
return 0;
|
||||
}
|
||||
if (props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] === null ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] === undefined) {
|
||||
return -1;
|
||||
}
|
||||
if (props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey] === null ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey] === undefined) {
|
||||
return 1;
|
||||
}
|
||||
return props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey].localeCompare(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey]);
|
||||
}catch(error){
|
||||
console.log("failed to parse data for sorting", error);
|
||||
return props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] > props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey] ? 1 : -1
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
if (sortData.sortDirection === 'DESC') {
|
||||
tempData.reverse();
|
||||
}
|
||||
return tempData;
|
||||
}, [allData, sortData]);
|
||||
const onSubmitFilterOptions = (value) => {
|
||||
setFilterOptions({...filterOptions, [selectedColumn.key]: value });
|
||||
try{
|
||||
updateSetting({setting_name: `${props.treeConfig.name}_browser_filter_options`, value: {...filterOptions, [selectedColumn.key]: value }});
|
||||
}catch(error){
|
||||
console.log("failed to save filter options");
|
||||
}
|
||||
}
|
||||
const filterRow = (row) => {
|
||||
if(props.treeRootData[props.selectedFolderData.group] === undefined ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host] === undefined ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row] === undefined) {
|
||||
return true;
|
||||
}
|
||||
if(!props.showDeletedFiles && props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.deleted){
|
||||
return true;
|
||||
}
|
||||
for(const [key,value] of Object.entries(filterOptions)){
|
||||
if(["name_text", "comment"].includes(key)){
|
||||
if(!String(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row][key]).toLowerCase().includes(value)){
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if(!String(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row].metadata[key]).toLowerCase().includes(value)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const gridData = React.useMemo(
|
||||
() =>
|
||||
// row is just the name
|
||||
sortedData.reduce((prev, row) => {
|
||||
if(filterRow(row)){
|
||||
return [...prev];
|
||||
}else{
|
||||
return [...prev, columns.map( c => {
|
||||
switch(c.type) {
|
||||
case "name":
|
||||
return <FileBrowserTableRowNameCell
|
||||
treeRootData={props.treeRootData[props.selectedFolderData.group]}
|
||||
selectedFolderData={props.selectedFolderData}
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row].name_text}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}/>;
|
||||
case "tags":
|
||||
return <FileBrowserTagsCell
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}
|
||||
treeRootData={props.treeRootData[props.selectedFolderData.group]}
|
||||
cellData={row}
|
||||
selectedFolderData={props.selectedFolderData}
|
||||
me={props.me}/>
|
||||
case "comment":
|
||||
return <FileBrowserTableRowStringCell
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row].comment}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}/>
|
||||
case "date":
|
||||
return <TableRowDateCell
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.metadata?.[c.key] || null}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}
|
||||
view_utc_time={props.me?.user?.view_utc_time}/>
|
||||
case "size":
|
||||
return <TableRowSizeCell
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.metadata?.[c.key] || 0}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}/>
|
||||
case "string":
|
||||
return <FileBrowserTableRowStringCell
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.metadata?.[c.key] || ""}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}/>
|
||||
default:
|
||||
return <FileBrowserTableRowStringCell
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.metadata?.[c.key] || ""}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}/>
|
||||
}
|
||||
})];
|
||||
}
|
||||
|
||||
}, []),
|
||||
[sortedData, props.onTaskRowAction, filterOptions, columnVisibility, props.showDeletedFiles]
|
||||
);
|
||||
const getDisplayFormat = () => {
|
||||
if(sortedData.length === 0 && props?.selectedFolderData?.success === false){
|
||||
return "showTask";
|
||||
}else if(sortedData.length > 0 || props?.selectedFolderData?.success){
|
||||
return "normal";
|
||||
} else if(sortedData.length === 0 && props?.selectedFolderData?.has_children && props?.selectedFolderData?.success === null){
|
||||
return "fetchLocal";
|
||||
}else if(sortedData.length === 0 && !props?.selectedFolderData?.has_children && props?.selectedFolderData?.success === null) {
|
||||
return "fetchRemote";
|
||||
}else if(sortedData.length === 0){
|
||||
return "fetchRemote";
|
||||
} else {
|
||||
return "normal";
|
||||
}
|
||||
}
|
||||
const displayFormat = getDisplayFormat();
|
||||
useEffect(() => {
|
||||
// when the folder changes, we need to aggregate all of the entries
|
||||
//console.log(props.selectedFolderData, props.treeAdjMatrix, props.treeRootData)
|
||||
let desiredPath = props.selectedFolderData.full_path_text;
|
||||
if(props.selectedFolderData.id === props.selectedFolderData.host){
|
||||
desiredPath = "";
|
||||
}
|
||||
let newAllData = Object.keys(props.treeAdjMatrix[props.selectedFolderData.group]?.[props.selectedFolderData.host]?.[desiredPath] || {});
|
||||
setAllData(newAllData);
|
||||
//console.log("just set all data")
|
||||
}, [props.selectedFolderData, props.treeAdjMatrix]);
|
||||
const onRowDoubleClick = (e, rowIndex, rowData) => {
|
||||
if (!rowData.can_have_children) {
|
||||
return;
|
||||
}
|
||||
snackActions.info('Fetching contents from database...');
|
||||
props.onRowDoubleClick({...rowData, group: props.selectedFolderData.group});
|
||||
setSortData({"sortKey": null, "sortType":null, "sortDirection": "ASC"});
|
||||
};
|
||||
const onClickHeader = (e, columnIndex) => {
|
||||
const column = columns[columnIndex];
|
||||
if(column.disableSort){
|
||||
return;
|
||||
}
|
||||
if (!column.key) {
|
||||
setSortData({"sortKey": null, "sortType":null, "sortDirection": "ASC", "inMetadata": false});
|
||||
}
|
||||
if (sortData.sortKey === column.key) {
|
||||
if (sortData.sortDirection === 'ASC') {
|
||||
setSortData({...sortData, "sortDirection": "DESC"});
|
||||
} else {
|
||||
setSortData({"sortKey": null, "sortType":null, "sortDirection": "ASC", "inMetadata": false});
|
||||
}
|
||||
} else {
|
||||
setSortData({"sortKey": column.key, "inMetadata": column.inMetadata, "sortType":column.type, "sortDirection": "ASC"});
|
||||
}
|
||||
};
|
||||
const contextMenuOptions = [
|
||||
{
|
||||
name: 'Filter Column', type: "item",
|
||||
icon: <FontAwesomeIcon icon={faFilter} style={{paddingRight: "5px"}} />,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
if(columns[columnIndex].disableFilterMenu){
|
||||
snackActions.warning("Can't filter that column");
|
||||
return;
|
||||
}
|
||||
setSelectedColumn(columns[columnIndex]);
|
||||
setOpenContextMenu(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Reorder Columns and Adjust Visibility", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
setOpenReorderDialog(true);
|
||||
}
|
||||
}
|
||||
];
|
||||
const sortColumn = columns.findIndex((column) => column.key === sortData.sortKey);
|
||||
const onRowClick = ({event, rowDataStatic}) => {
|
||||
if(event.ctrlKey || event.metaKey){
|
||||
if(props.treeRootData?.[props.selectedFolderData.group]?.[rowDataStatic.host]?.[rowDataStatic.full_path_text]){
|
||||
props.treeRootData[props.selectedFolderData.group][rowDataStatic.host][rowDataStatic.full_path_text].selected = true;
|
||||
}
|
||||
|
||||
setSelectedRows([...selectedRows, rowDataStatic]);
|
||||
} else {
|
||||
if(props.treeRootData?.[props.selectedFolderData.group]?.[rowDataStatic.host]?.[rowDataStatic.full_path_text]){
|
||||
props.treeRootData[props.selectedFolderData.group][rowDataStatic.host][rowDataStatic.full_path_text].selected = true;
|
||||
}
|
||||
for(let i = 0; i < selectedRows.length; i++){
|
||||
if(props.treeRootData?.[props.selectedFolderData.group]?.[selectedRows[i].host]?.[selectedRows[i].full_path_text]){
|
||||
props.treeRootData[props.selectedFolderData.group][selectedRows[i].host][selectedRows[i].full_path_text].selected = false;
|
||||
}
|
||||
}
|
||||
setSelectedRows([rowDataStatic]);
|
||||
}
|
||||
}
|
||||
const optionsA = (element) => [
|
||||
{
|
||||
name: element?.name_text, icon: null, click: ({event}) => {},
|
||||
type: "item", disabled: true
|
||||
},
|
||||
{
|
||||
name: "Copy to Clipboard", icon: null, click: () => {}, type: "menu",
|
||||
menuItems: [
|
||||
{
|
||||
name: 'Name', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(element.name_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Full Path', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(element.full_path_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Metadata', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(JSON.stringify(element?.metadata, null, 2))){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'View Metadata', type: "item",
|
||||
icon: <VisibilityIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
permissionDataRef.current.metadata = element?.metadata;
|
||||
permissionDataRef.current.name = element.name_text;
|
||||
setViewPermissionsDialogOpen(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Edit Comment', type: "item",
|
||||
icon: <EditIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
commentDataRef.current.id = element.id;
|
||||
commentDataRef.current.comment = element.comment;
|
||||
setFileCommentDialogOpen(true);
|
||||
},
|
||||
},
|
||||
]
|
||||
async function optionsB (callback_id, callback_display_id, element) {
|
||||
let downloadCommand = await props.getLoadedCommandForUIFeature(callback_id, element.can_have_children ? props.baseUIFeature + ":download_folder" : props.baseUIFeature + ":download");
|
||||
let downloadDisplay = "Download (Unsupported)";
|
||||
if(element.can_have_children){
|
||||
downloadDisplay = "Download Folder (Unsupported)";
|
||||
}
|
||||
if(downloadCommand !== undefined){
|
||||
downloadDisplay = `Download (${downloadCommand.command.cmd})`;
|
||||
if(element.can_have_children){
|
||||
downloadDisplay = `Download Folder (${downloadCommand.command.cmd})`;
|
||||
}
|
||||
}
|
||||
let listCommand = await props.getLoadedCommandForUIFeature(callback_id, props.baseUIFeature + ":list");
|
||||
let listDisplay = "List (Unsupported)";
|
||||
if(listCommand !== undefined){
|
||||
listDisplay = `List (${listCommand.command.cmd})`;
|
||||
}
|
||||
let removeCommand = await props.getLoadedCommandForUIFeature(callback_id, props.baseUIFeature + ":remove");
|
||||
let removeDisplay = "Remove (Unsupported)";
|
||||
if(removeCommand !== undefined){
|
||||
removeDisplay = `Remove (${removeCommand.command.cmd})`;
|
||||
}
|
||||
return [
|
||||
{
|
||||
name: downloadDisplay, type: "item",
|
||||
disabled: downloadCommand === undefined,
|
||||
icon: <GetAppIcon color="success" style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
props.onTaskRowAction({
|
||||
path: element.parent_path_text,
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: element.can_have_children ? props.baseUIFeature + ':download_folder' : props.baseUIFeature + ':download',
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: listDisplay, type: "item",
|
||||
disabled: listCommand === undefined,
|
||||
icon: <ListIcon color="warning" style={{ paddingRight: '5px'}} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
props.onTaskRowAction({
|
||||
path: element.parent_path_text,
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: props.baseUIFeature + ':list',
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: removeDisplay, type: "item",
|
||||
disabled: removeCommand === undefined,
|
||||
icon: <DeleteIcon color="error" style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
props.onTaskRowAction({
|
||||
path: element.parent_path_text,
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: props.baseUIFeature + ':remove',
|
||||
getConfirmation: true,
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
async function optionsC (callback_id, callback_display_id, element, action_name, ui_feature, icon, color, openDialog, getConfirmation) {
|
||||
let command = await props.getLoadedCommandForUIFeature(callback_id, ui_feature);
|
||||
let commandDisplay = `${action_name} (Unsupported)`;
|
||||
if(command !== undefined){
|
||||
commandDisplay = `${action_name} (${command.command.cmd})`;
|
||||
}
|
||||
return {
|
||||
name: commandDisplay, type: "item",
|
||||
disabled: command === undefined,
|
||||
icon: <FontAwesomeIcon icon={getIconName(icon)} style={{marginRight: "5px", color: getIconColor(theme, color || "")}}/>,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
props.onTaskRowAction({
|
||||
path: element.parent_path_text,
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: ui_feature, openDialog, getConfirmation,
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
async function onRowContextClick({rowDataStatic}) {
|
||||
// based on row, return updated options array?
|
||||
if(selectedRows.length > 1){
|
||||
let downloadCommand = await props.getLoadedCommandForUIFeature(rowDataStatic.callback.id, rowDataStatic.can_have_children ? props.baseUIFeature + ":download_folder" : props.baseUIFeature + ":download");
|
||||
let listCommand = await props.getLoadedCommandForUIFeature(rowDataStatic.callback.id, props.baseUIFeature + ":list");
|
||||
let removeCommand = await props.getLoadedCommandForUIFeature(rowDataStatic.callback.id, props.baseUIFeature + ":remove");
|
||||
return [
|
||||
{
|
||||
name: `Download All Selected`, type: "item",
|
||||
disabled: downloadCommand === undefined,
|
||||
icon: <GetAppIcon color="success" style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
let newTasks = [];
|
||||
for(let i = 0; i < selectedRows.length; i++){
|
||||
newTasks.push({
|
||||
path: selectedRows[i].parent_path_text,
|
||||
full_path: selectedRows[i].full_path_text,
|
||||
host: selectedRows[i].host,
|
||||
filename: selectedRows[i].name_text,
|
||||
uifeature: selectedRows[i].can_have_children ? props.baseUIFeature + ':download_folder': props.baseUIFeature + ':download',
|
||||
callback_id: props.tabInfo.callbackID,
|
||||
callback_display_id: props.tabInfo.displayID
|
||||
});
|
||||
}
|
||||
props.onTaskRowActions(newTasks);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `Remove All Selected`, type: "item",
|
||||
disabled: removeCommand === undefined,
|
||||
icon: <DeleteIcon color="error" style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
let newTasks = [];
|
||||
for(let i = 0; i < selectedRows.length; i++){
|
||||
newTasks.push({
|
||||
path: selectedRows[i].parent_path_text,
|
||||
full_path: selectedRows[i].full_path_text,
|
||||
host: selectedRows[i].host,
|
||||
filename: selectedRows[i].name_text,
|
||||
uifeature: props.baseUIFeature + ':remove',
|
||||
getConfirmation: true,
|
||||
callback_id: props.tabInfo.callbackID,
|
||||
callback_display_id: props.tabInfo.displayID
|
||||
});
|
||||
}
|
||||
props.onTaskRowActions(newTasks);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `List All Selected`, type: "item",
|
||||
disabled: listCommand === undefined,
|
||||
icon: <ListIcon color="warning" style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
let newTasks = [];
|
||||
for(let i = 0; i < selectedRows.length; i++){
|
||||
newTasks.push({
|
||||
path: selectedRows[i].parent_path_text,
|
||||
full_path: selectedRows[i].full_path_text,
|
||||
host: selectedRows[i].host,
|
||||
filename: selectedRows[i].name_text,
|
||||
uifeature: props.baseUIFeature + ':list',
|
||||
callback_id: props.tabInfo.callbackID,
|
||||
callback_display_id: props.tabInfo.displayID
|
||||
});
|
||||
}
|
||||
props.onTaskRowActions(newTasks);
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
let options = [...optionsA(rowDataStatic)];
|
||||
options.push(...(await optionsB(props.tabInfo.callbackID, props.tabInfo.displayID, rowDataStatic)));
|
||||
if(props.treeConfig.row_actions.length > 0){
|
||||
let extraOptions = [];
|
||||
for(let i = 0; i < props.treeConfig.row_actions.length; i++){
|
||||
let r = props.treeConfig.row_actions[i];
|
||||
if((r.supports_file && !rowDataStatic.can_have_children) || (r.supports_folder && rowDataStatic.can_have_children) ){
|
||||
extraOptions.push(await optionsC(rowDataStatic.callback.id, rowDataStatic.callback.display_id,
|
||||
rowDataStatic, r.name, r.ui_feature, r.icon, r.color, r.openDialog, r.getConfirmation));
|
||||
}
|
||||
}
|
||||
if(extraOptions.length > 0){
|
||||
options.push({
|
||||
name: `Custom Actions`, icon: null, click: () => {}, type: "menu",
|
||||
menuItems: extraOptions
|
||||
})
|
||||
}
|
||||
}
|
||||
if(rowDataStatic.callback.display_id !== props.tabInfo.displayID){
|
||||
options.push({
|
||||
name: `Original Callback: ${rowDataStatic.callback.display_id}`, icon: null, click: () => {}, type: "menu",
|
||||
menuItems: [
|
||||
...(await optionsB(rowDataStatic.callback.id, rowDataStatic.callback.display_id, rowDataStatic))
|
||||
]
|
||||
})
|
||||
}
|
||||
return options;
|
||||
}
|
||||
React.useEffect( () => {
|
||||
// on startup, want to see if `callbacks_table_columns` exists in storage and load it if possible
|
||||
try {
|
||||
const storageItem = GetMythicSetting({setting_name: `${props.treeConfig.name}_browser_table_columns`, default_value: [...props.treeConfig.table.visible]});
|
||||
if(storageItem !== null){
|
||||
let allColumns = [...columnVisibility["visible"].map(c => c), ...columnVisibility["hidden"].map(c => c)];
|
||||
let newHidden = [];
|
||||
allColumns.forEach((v,i,a) => {
|
||||
if(!storageItem.includes(v)){
|
||||
newHidden.push(v);
|
||||
}
|
||||
});
|
||||
if(storageItem.length !== 0){
|
||||
setColumnVisibility({visible: storageItem, hidden: newHidden});
|
||||
}
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_columns", error);
|
||||
}
|
||||
try {
|
||||
const storageItemOptions = GetMythicSetting({setting_name: `${props.treeConfig.name}_browser_filter_options`, default_value: {}});
|
||||
if(storageItemOptions !== null){
|
||||
setFilterOptions(storageItemOptions);
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_filter_options", error);
|
||||
}
|
||||
try {
|
||||
const storageColumnOrder = GetMythicSetting({setting_name: `${props.treeConfig.name}_browser_column_order`, default_value: columns.map(c => c.name)});
|
||||
if(storageColumnOrder !== null){
|
||||
let newOrder = [];
|
||||
for(let i = 0; i < storageColumnOrder.length; i++){
|
||||
for(let j = 0; j < columnOrder.length; j++){
|
||||
if(columnOrder[j].name === storageColumnOrder[i]){
|
||||
newOrder.push(columnOrder[j]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(newOrder.length === columns.length){
|
||||
setColumnOrder(newOrder);
|
||||
}
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_filter_options", error);
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
const onSubmitColumnReorder = (newOrder) => {
|
||||
let newVisible = [];
|
||||
let newHidden = [];
|
||||
for(let i = 0; i < newOrder.length; i++){
|
||||
if(newOrder[i].visible){
|
||||
newVisible.push(newOrder[i].name);
|
||||
} else {
|
||||
newHidden.push(newOrder[i].name);
|
||||
}
|
||||
}
|
||||
if(newVisible.length === 0){
|
||||
snackActions.error("Can't update to show no fields");
|
||||
return;
|
||||
}
|
||||
updateSetting({setting_name: `${props.treeConfig.name}_browser_column_order`, value: newOrder.map(c => c.name)});
|
||||
setColumnOrder(newOrder);
|
||||
setColumnVisibility({visible: newVisible, hidden: newHidden});
|
||||
updateSetting({setting_name: `${props.treeConfig.name}_browser_table_columns`, value: newVisible});
|
||||
setOpenReorderDialog(false);
|
||||
}
|
||||
const onResetColumnReorder = () => {
|
||||
onSubmitColumnReorder(initialDefaultColumns);
|
||||
}
|
||||
if(loading){
|
||||
return (
|
||||
<div style={{width: '100%', height: '100%', position: "relative",}}>
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%"
|
||||
}}>
|
||||
{"Loading Saved Browser Customizations..."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div style={{width: '100%', height: '100%', overflow: "hidden", position: "relative"}}>
|
||||
{displayFormat === "normal" &&
|
||||
<>
|
||||
<MythicResizableGrid
|
||||
columns={columns}
|
||||
sortIndicatorIndex={sortColumn}
|
||||
sortDirection={sortData.sortDirection}
|
||||
items={gridData}
|
||||
rowHeight={GetComputedFontSize() + 7}
|
||||
onClickHeader={onClickHeader}
|
||||
onDoubleClickRow={onRowDoubleClick}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
onRowContextMenuClick={onRowContextClick}
|
||||
onRowClick={onRowClick}
|
||||
/>
|
||||
{fileCommentDialogOpen && (
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
maxWidth='md'
|
||||
open={fileCommentDialogOpen}
|
||||
onClose={() => {
|
||||
setFileCommentDialogOpen(false);
|
||||
}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Edit Comment'
|
||||
onSubmit={onSubmitUpdatedComment}
|
||||
value={commentDataRef.current?.value}
|
||||
onClose={() => {
|
||||
setFileCommentDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{viewPermissionsDialogOpen && (
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
maxWidth='lg'
|
||||
open={viewPermissionsDialogOpen}
|
||||
onClose={() => {
|
||||
setViewPermissionsDialogOpen(false);
|
||||
}}
|
||||
innerDialog={
|
||||
<MythicViewJSONAsTableDialog
|
||||
title={'View Metadata for ' + permissionDataRef.current?.name}
|
||||
leftColumn='Attribute'
|
||||
rightColumn='Value'
|
||||
value={permissionDataRef.current?.metadata}
|
||||
onClose={() => {
|
||||
setViewPermissionsDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{openReorderDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openReorderDialog}
|
||||
onClose={()=>{setOpenReorderDialog(false);}}
|
||||
innerDialog={
|
||||
<CallbacksTableColumnsReorderDialog
|
||||
onClose={()=>{setOpenReorderDialog(false);}}
|
||||
visible={columnVisibility.visible}
|
||||
hidden={columnVisibility.hidden}
|
||||
onReset={onResetColumnReorder}
|
||||
onSubmit={onSubmitColumnReorder}
|
||||
initialItems={columnOrder}
|
||||
/>}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
}
|
||||
{displayFormat === "normal" && props?.selectedFolderData?.success === null && indicatePartialListingRef.current &&
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid black",
|
||||
padding: "5px",
|
||||
backgroundColor: "rgba(37,37,37,0.92)", color: "white",
|
||||
}}>
|
||||
{"Only PARTIAL data has been collected for this path. "}<br/>
|
||||
{"Task this callback to list the contents"}
|
||||
<IconButton style={{margin: 0, padding: 0, marginRight: "10px"}}
|
||||
onClick={props.onListFilesButtonFromTableWithNoEntries}>
|
||||
<RefreshIcon color={"info"} fontSize={"large"}
|
||||
style={{display: "inline-block",}}/>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{displayFormat === "fetchLocal" && indicatePartialListingRef.current &&
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid black",
|
||||
padding: "5px",
|
||||
backgroundColor: "rgba(37,37,37,0.92)", color: "white",
|
||||
}}>
|
||||
{"Some data exists for this path, but isn't loaded into the UI. "}
|
||||
<br/>
|
||||
{"Click the folder icon to fetch data from the database."}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{displayFormat === "fetchRemote" && indicatePartialListingRef.current &&
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid black",
|
||||
padding: "5px",
|
||||
backgroundColor: "rgba(37,37,37,0.92)", color: "white",
|
||||
}}>
|
||||
{"No data has been collected for this path. "}
|
||||
<div style={{display: "flex", alignItems: "center"}}>
|
||||
{"Task this callback to list the contents"}
|
||||
<IconButton style={{margin: 0, padding: 0, marginRight: "10px"}} onClick={props.onListFilesButtonFromTableWithNoEntries} >
|
||||
<RefreshIcon color={"info"} fontSize={"large"}
|
||||
style={{ display: "inline-block",}} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{displayFormat === "showTask" && props.selectedFolderData?.task_id > 0 &&
|
||||
<RenderSingleTask task_id={props.selectedFolderData?.task_id} />
|
||||
}
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openContextMenu}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={filterOptions[selectedColumn.key]}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const FileBrowserTableRowNameCell = ({cellData, rowData, treeRootData, selectedFolderData }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div style={{ alignItems: 'center', display: 'flex', maxHeight: "100%", textDecoration: treeRootData[selectedFolderData.host][rowData.full_path_text]?.deleted ? 'line-through' : '' }}>
|
||||
{!treeRootData[selectedFolderData.host][rowData.full_path_text]?.can_have_children ? (
|
||||
<DescriptionIcon size={"small"} style={{
|
||||
marginRight: '5px' }} />
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={faFolder}
|
||||
size={"1x"}
|
||||
style={{
|
||||
marginRight: '8px',
|
||||
marginLeft: "4px",
|
||||
color:
|
||||
treeRootData[selectedFolderData.host][rowData.full_path_text]?.success || treeRootData[selectedFolderData.host][rowData.full_path_text]?.has_children
|
||||
? theme.folderColor
|
||||
: theme.emptyFolderColor,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<pre
|
||||
style={{
|
||||
color:
|
||||
treeRootData[selectedFolderData.host][rowData.full_path_text]?.success !== null
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary,
|
||||
}}>
|
||||
{cellData}
|
||||
</pre>
|
||||
{treeRootData[selectedFolderData.host][rowData.full_path_text]?.success === true ? (
|
||||
<MythicStyledTooltip title='Successfully listed contents of folder' tooltipStyle={{display: "inline-flex", marginLeft: "5px"}}>
|
||||
<CheckCircleOutlineIcon color="success" fontSize='small' />
|
||||
</MythicStyledTooltip>
|
||||
) : treeRootData[selectedFolderData.host][rowData.full_path_text]?.success === false ? (
|
||||
<MythicStyledTooltip title='Failed to list contents of folder' tooltipStyle={{display: "inline-flex", marginLeft: "5px"}}>
|
||||
<ErrorIcon fontSize='small' color="error" />
|
||||
</MythicStyledTooltip>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const FileBrowserTagsCell = ({rowData, cellData, treeRootData, selectedFolderData, me}) => {
|
||||
return (
|
||||
<>
|
||||
<ViewEditTags target_object={"mythictree_id"} target_object_id={treeRootData[selectedFolderData.host][cellData]?.id} me={me} />
|
||||
<TagsDisplay tags={treeRootData[selectedFolderData.host][cellData]?.tags} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
const FileBrowserTableRowStringCell = ({ cellData, rowData }) => {
|
||||
//console.log(rowData, cellData)
|
||||
return (
|
||||
<>
|
||||
{cellData}
|
||||
</>
|
||||
)
|
||||
};
|
||||
export const TableRowDateCell = ({ cellData, rowData, view_utc_time }) => {
|
||||
|
||||
try{
|
||||
let cellDataInt = parseInt(cellData)
|
||||
if(cellData === "" || cellData === undefined || cellDataInt <= 0){
|
||||
return "";
|
||||
}
|
||||
let view_utc = true;
|
||||
if(view_utc_time !== undefined){
|
||||
view_utc = view_utc_time
|
||||
}
|
||||
// handle Unix epoch timestamps
|
||||
if (view_utc) {
|
||||
let init_date = new Date(cellDataInt);
|
||||
return init_date.toDateString() + " " + init_date.toTimeString().substring(0, 8) + " UTC";
|
||||
} else {
|
||||
let timezoneDate = new Date(cellDataInt);
|
||||
timezoneDate.setTime(timezoneDate.getTime() - (timezoneDate.getTimezoneOffset() *60*1000));
|
||||
return timezoneDate.toLocaleDateString() + " " + timezoneDate.toLocaleString([], {hour12: true, hour: "2-digit", minute: "2-digit"});
|
||||
}
|
||||
//const dateData = new Date(cellDataInt).toISOString();
|
||||
//return toLocalTime(dateData.slice(0, 10) + " " + dateData.slice(11,-1), view_utc_time);
|
||||
}catch(error){
|
||||
try{
|
||||
let cellDataInt = parseInt(cellData)
|
||||
// handle windows FILETIME values
|
||||
const dateData = new Date( ((cellDataInt / 10000000) - 11644473600) * 1000).toISOString();
|
||||
return toLocalTime(dateData.slice(0, 10) + " " + dateData.slice(11,-1), view_utc_time);
|
||||
}catch(error2){
|
||||
console.log("error with timestamp: ", cellData);
|
||||
return String(cellData);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
export const TableRowSizeCell = ({ cellData, rowData }) => {
|
||||
const getStringSize = () => {
|
||||
try {
|
||||
// process for getting human readable string from bytes: https://stackoverflow.com/a/18650828
|
||||
let bytes = parseInt(cellData);
|
||||
if (cellData === '' || cellData === undefined) return '';
|
||||
if (bytes === 0) return '0 B';
|
||||
const decimals = 2;
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
} catch (error) {
|
||||
return cellData;
|
||||
}
|
||||
};
|
||||
return getStringSize(cellData);
|
||||
};
|
||||
@@ -47,10 +47,12 @@ const fileDataFragment = gql`
|
||||
color
|
||||
id
|
||||
}
|
||||
id
|
||||
}
|
||||
host
|
||||
id
|
||||
can_have_children
|
||||
has_children
|
||||
success
|
||||
full_path_text
|
||||
name_text
|
||||
@@ -68,7 +70,7 @@ const fileDataFragment = gql`
|
||||
const rootFileQuery = gql`
|
||||
${fileDataFragment}
|
||||
query myRootFolderQuery {
|
||||
mythictree(where: { parent_path_text: { _eq: "" }, tree_type: {_eq: "file"} }) {
|
||||
mythictree(where: { parent_path_text: { _eq: "" }, tree_type: {_eq: "file"} }, order_by: {id: desc}) {
|
||||
...fileObjData
|
||||
}
|
||||
}
|
||||
@@ -196,7 +198,7 @@ export function CallbacksTabsFileBrowserLabel(props) {
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNewDataForTab }) => {
|
||||
export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNewDataForTab, baseUIFeature }) => {
|
||||
const fromNow = React.useRef(getSkewedNow());
|
||||
const [backdropOpen, setBackdropOpen] = React.useState(false);
|
||||
const treeRootDataRef = React.useRef({}); // hold all of the actual data
|
||||
@@ -215,7 +217,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
token: 0,
|
||||
});
|
||||
const autoTaskLsOnEmptyDirectoriesRef = React.useRef(false);
|
||||
const taskingData = React.useRef({"parameters": "", "ui_feature": "file_browser:list"});
|
||||
const taskingData = React.useRef({"parameters": "", "ui_feature": baseUIFeature +":list"});
|
||||
const mountedRef = React.useRef(true);
|
||||
const tableOpenedPathIdRef = React.useRef({
|
||||
host: "",
|
||||
@@ -255,6 +257,10 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] = {...data.mythictree[i]}
|
||||
}
|
||||
}
|
||||
if(data.mythictree[i].id > treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]].id){
|
||||
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] |= data.mythictree[i].has_children;
|
||||
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]].success |= data.mythictree[i].success;
|
||||
}
|
||||
}
|
||||
}
|
||||
// create the top level data in the adjacency matrix
|
||||
@@ -315,18 +321,19 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
} else {
|
||||
// we need to merge data in because we already have some info
|
||||
let existingData = treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]];
|
||||
if(existingData.task_id < data.data.mythictree_stream[i].task_id){
|
||||
if(existingData.task_id <= data.data.mythictree_stream[i].task_id){
|
||||
existingData.deleted = data.data.mythictree_stream[i].deleted;
|
||||
}
|
||||
if( (existingData.success === null || !existingData.success) && data.data.mythictree_stream[i].success !== null){
|
||||
existingData.success = data.data.mythictree_stream[i].success;
|
||||
existingData.task_id = data.data.mythictree_stream[i].task_id;
|
||||
}
|
||||
existingData.comment += data.data.mythictree_stream[i].comment;
|
||||
existingData.comment = data.data.mythictree_stream[i].comment;
|
||||
existingData.tags = [...existingData.tags, ...data.data.mythictree_stream[i].tags];
|
||||
if(!existingData.metadata.has_children){
|
||||
existingData.metadata.has_children = data.data.mythictree_stream[i].metadata.has_children;
|
||||
}
|
||||
existingData.has_children = data.data.mythictree_stream[i].has_children || existingData.has_children;
|
||||
existingData.metadata.size = data.data.mythictree_stream[i].metadata.size;
|
||||
existingData.metadata.access_time = data.data.mythictree_stream[i].metadata.access_time;
|
||||
existingData.metadata.modify_time = data.data.mythictree_stream[i].metadata.modify_time;
|
||||
if(data.data.mythictree_stream[i].metadata.permissions !== undefined && data.data.mythictree_stream[i].metadata.permissions !== null){
|
||||
if(Array.isArray(existingData.metadata.permissions) && Array.isArray(data.data.mythictree_stream[i].metadata.permissions)){
|
||||
existingData.metadata.permissions = [...existingData.metadata.permissions, ...data.data.mythictree_stream[i].metadata.permissions];
|
||||
@@ -455,7 +462,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
} else {
|
||||
// we need to merge data in because we already have some info
|
||||
let existingData = treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]][mythictree[i]["full_path_text"]];
|
||||
if(existingData.task_id < mythictree[i].task_id){
|
||||
if(existingData.task_id <= mythictree[i].task_id){
|
||||
existingData.deleted = mythictree[i].deleted;
|
||||
}
|
||||
if( (existingData.success === null || !existingData.success) && mythictree[i].success !== null){
|
||||
@@ -464,9 +471,10 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
}
|
||||
existingData.comment += mythictree[i].comment;
|
||||
existingData.tags = [...existingData.tags, ...mythictree[i].tags];
|
||||
if(!existingData.metadata.has_children){
|
||||
existingData.metadata.has_children = mythictree[i].metadata.has_children;
|
||||
}
|
||||
existingData.has_children = mythictree[i].has_children || existingData.has_children;
|
||||
existingData.metadata.size = mythictree[i].metadata.size;
|
||||
existingData.metadata.access_time = mythictree[i].metadata.access_time;
|
||||
existingData.metadata.modify_time = mythictree[i].metadata.modify_time;
|
||||
//console.log("updating permissions", "existing", existingData.metadata.permissions, "new", mythictree[i].metadata)
|
||||
if(mythictree[i].metadata.permissions !== undefined && mythictree[i].metadata.permissions !== null){
|
||||
if(Array.isArray(existingData.metadata.permissions) && Array.isArray(mythictree[i].metadata.permissions)){
|
||||
@@ -575,7 +583,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
taskingData.current = ({
|
||||
"token": token,
|
||||
"parameters": {path: path, full_path: path, host: selectedFolderData.host, file: ""},
|
||||
"ui_feature": "file_browser:list", callback_id, callback_display_id});
|
||||
"ui_feature": baseUIFeature + ":list", callback_id, callback_display_id});
|
||||
setOpenTaskingButton(true);
|
||||
};
|
||||
const onListFilesButtonFromTableWithNoEntries = () => {
|
||||
@@ -587,7 +595,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
host: selectedFolderData.host,
|
||||
file: ""
|
||||
},
|
||||
"ui_feature": "file_browser:list",
|
||||
"ui_feature": baseUIFeature +":list",
|
||||
callback_id: tabInfo.callbackID,
|
||||
callback_display_id: tabInfo.displayID});
|
||||
setOpenTaskingButton(true);
|
||||
@@ -596,7 +604,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
taskingData.current = ({
|
||||
"token": token,
|
||||
"parameters": {path: fullPath, full_path: fullPath, host: selectedFolderData.host},
|
||||
"ui_feature": "file_browser:upload", "openDialog": true,
|
||||
"ui_feature": baseUIFeature + ":upload", "openDialog": true,
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
setOpenTaskingButton(true);
|
||||
@@ -656,7 +664,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
taskingData.current = ({
|
||||
"token": localSelectedToken.current,
|
||||
"parameters": {path: nodeData.full_path_text, full_path: nodeData.full_path_text, host: nodeData.host, file: ""},
|
||||
"ui_feature": "file_browser:list", callback_id, callback_display_id});
|
||||
"ui_feature": baseUIFeature + ":list", callback_id, callback_display_id});
|
||||
setOpenTaskingButton(true);
|
||||
}
|
||||
const openDirectoryPath = ({group, host, path}) => {
|
||||
@@ -690,6 +698,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
taskListing={taskListing}
|
||||
tableOpenedPathId={tableOpenedPathIdRef.current}
|
||||
getLoadedCommandForUIFeature={getLoadedCommandForUIFeature}
|
||||
baseUIFeature={baseUIFeature}
|
||||
/>
|
||||
|
||||
</div>
|
||||
@@ -708,6 +717,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
fetchParentFolderData={fetchParentFolderData}
|
||||
fetchFolderData={fetchFolderData}
|
||||
openDirectoryPath={openDirectoryPath}
|
||||
baseUIFeature={baseUIFeature}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flexGrow: 1 }}>
|
||||
@@ -727,6 +737,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
onTaskRowActions={onTaskRowActions}
|
||||
getLoadedCommandForUIFeature={getLoadedCommandForUIFeature}
|
||||
me={me}
|
||||
baseUIFeature={baseUIFeature}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -758,7 +769,8 @@ const FileBrowserTableTop = ({
|
||||
fetchParentFolderData,
|
||||
fetchFolderData,
|
||||
openDirectoryPath,
|
||||
tabInfo
|
||||
tabInfo,
|
||||
baseUIFeature
|
||||
}) => {
|
||||
const autoTaskLsOnEmptyDirectories = useGetMythicSetting({
|
||||
setting_name: "autoTaskLsOnEmptyDirectories", default_value: false
|
||||
@@ -794,6 +806,9 @@ const FileBrowserTableTop = ({
|
||||
shouldResubscribe: true,
|
||||
onData: ({data}) => {
|
||||
setTokenOptions(data.data.callbacktoken);
|
||||
if(data.data.callbacktoken.length === 0) {
|
||||
onChangeSelectedToken("Default Token");
|
||||
}
|
||||
}
|
||||
});
|
||||
useEffect(() => {
|
||||
@@ -964,9 +979,10 @@ const FileBrowserTableTop = ({
|
||||
borderRight: "1px solid grey",
|
||||
marginRight: "10px",
|
||||
padding: "0 5px 0 0"}}>
|
||||
{tokenOptions.length > 0 ? (
|
||||
<CallbacksTabsTaskingInputTokenSelect options={tokenOptions} changeSelectedToken={changeSelectedToken}/>
|
||||
) : null}
|
||||
{tokenOptions.length > 0 &&
|
||||
<CallbacksTabsTaskingInputTokenSelect width={"100%"}
|
||||
options={tokenOptions} changeSelectedToken={changeSelectedToken}/>
|
||||
}
|
||||
<MythicStyledTooltip title={`Move back to previous listing`}>
|
||||
<IconButton style={{ padding: '3px' }}
|
||||
disabled={historyIndex >= history.length -1 }
|
||||
|
||||
@@ -26,8 +26,6 @@ import FileCopyOutlinedIcon from '@mui/icons-material/FileCopyOutlined';
|
||||
import { copyStringToClipboard } from '../../utilities/Clipboard';
|
||||
import MythicResizableGrid from '../../MythicComponents/MythicResizableGrid';
|
||||
import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip';
|
||||
import {TableFilterDialog} from './TableFilterDialog';
|
||||
import {MythicTransferListDialog} from '../../MythicComponents/MythicTransferList';
|
||||
import {TagsDisplay, ViewEditTags} from '../../MythicComponents/MythicTag';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import { toLocalTime } from '../../utilities/Time';
|
||||
@@ -37,7 +35,12 @@ import {PreviewFileMediaDialog} from "../Search/PreviewFileMedia";
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import {Dropdown, DropdownMenuItem, DropdownNestedMenuItem} from "../../MythicComponents/MythicNestedMenus";
|
||||
import {RenderSingleTask} from "../SingleTaskView/SingleTaskView";
|
||||
import {GetComputedFontSize} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {
|
||||
GetComputedFontSize,
|
||||
GetMythicSetting,
|
||||
useSetMythicSetting
|
||||
} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {CallbacksTableColumnsReorderDialog} from "./CallbacksTableColumnsReorderDialog";
|
||||
|
||||
const getFileDownloadHistory = gql`
|
||||
query getFileDownloadHistory($full_path_text: String!, $host: String!, $group: [String!]) {
|
||||
@@ -76,28 +79,33 @@ const updateFileComment = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
const defaultVisibleColumns = ["Info", "Name", "Size", "Last Modify"];
|
||||
const columnDefaults = [
|
||||
{ name: 'Info', width: 65, disableDoubleClick: true, disableSort: true, disableFilterMenu: true, key: "info", visible: true },
|
||||
{ name: 'Name', type: 'string', key: 'name_text', fillWidth: true, visible: true },
|
||||
{ name: "Size", type: "size", key: "size", inMetadata: true, width: 100, visible: true},
|
||||
{ name: "Last Modify", type: "date", key: "modify_time", inMetadata: true, width: 250, visible: true},
|
||||
{ name: 'Tags', type: 'tags', disableSort: true, disableFilterMenu: true, width: 220, key: "tags", visible: false },
|
||||
{ name: 'Comment', type: 'string', key: 'comment', width: 200, visible: false },
|
||||
];
|
||||
export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
const [updateSetting] = useSetMythicSetting();
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [allData, setAllData] = React.useState([]);
|
||||
const [openContextMenu, setOpenContextMenu] = React.useState(false);
|
||||
const [filterOptions, setFilterOptions] = React.useState({});
|
||||
const [selectedColumn, setSelectedColumn] = React.useState({});
|
||||
const [sortData, setSortData] = React.useState({"sortKey": null, "sortDirection": null, "sortType": null})
|
||||
const [columnVisibility, setColumnVisibility] = React.useState({
|
||||
"visible": ["Info", "Name", "Size", "Last Modify"],
|
||||
"visible": defaultVisibleColumns,
|
||||
"hidden": ["Tags", "Comment"]
|
||||
});
|
||||
const [openAdjustColumnsDialog, setOpenAdjustColumnsDialog] = React.useState(false);
|
||||
const [openReorderDialog, setOpenReorderDialog] = React.useState(false);
|
||||
const [columnOrder, setColumnOrder] = React.useState(columnDefaults);
|
||||
const [selectedRows, setSelectedRows] = React.useState([]);
|
||||
const columns = React.useMemo(
|
||||
() =>
|
||||
[
|
||||
{ name: 'Info', width: 65, disableDoubleClick: true, disableSort: true, disableFilterMenu: true },
|
||||
{ name: 'Name', type: 'string', key: 'name_text', fillWidth: true },
|
||||
{ name: "Size", type: "size", key: "size", inMetadata: true, width: 100},
|
||||
{ name: "Last Modify", type: "date", key: "modify_time", inMetadata: true, width: 250},
|
||||
{ name: 'Tags', type: 'tags', disableSort: true, disableFilterMenu: true, width: 220 },
|
||||
{ name: 'Comment', type: 'string', key: 'comment', width: 200 },
|
||||
].reduce( (prev, cur) => {
|
||||
columnOrder.reduce( (prev, cur) => {
|
||||
if(columnVisibility.visible.includes(cur.name)){
|
||||
if(filterOptions[cur.key] && String(filterOptions[cur.key]).length > 0){
|
||||
return [...prev, {...cur, filtered: true}];
|
||||
@@ -108,7 +116,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
return [...prev];
|
||||
}
|
||||
}, [])
|
||||
, [filterOptions, columnVisibility]
|
||||
, [filterOptions, columnVisibility, columnOrder]
|
||||
);
|
||||
const sortedData = React.useMemo(() => {
|
||||
if (sortData.sortKey === null || sortData.sortType === null) {
|
||||
@@ -188,10 +196,22 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
}
|
||||
return tempData;
|
||||
}, [allData, sortData]);
|
||||
const onSubmitFilterOptions = (newFilterOptions) => {
|
||||
setFilterOptions(newFilterOptions);
|
||||
const onSubmitFilterOptions = (value) => {
|
||||
|
||||
try{
|
||||
let newValue = value ? value : "";
|
||||
setFilterOptions({...filterOptions, [selectedColumn.key]: newValue });
|
||||
updateSetting({setting_name: `file_browser_filter_options`, value: {...filterOptions,[selectedColumn.key]: newValue }});
|
||||
}catch(error){
|
||||
console.log("failed to save filter options", error);
|
||||
}
|
||||
}
|
||||
const filterRow = (row) => {
|
||||
if(props.treeRootData[props.selectedFolderData.group] === undefined ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host] === undefined ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row] === undefined) {
|
||||
return true;
|
||||
}
|
||||
if(!props.showDeletedFiles && props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.deleted){
|
||||
return true;
|
||||
}
|
||||
@@ -216,7 +236,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
treeRootData={props.treeRootData[props.selectedFolderData.group]}
|
||||
selectedFolderData={props.selectedFolderData}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}
|
||||
cellData={row}
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.name_text}
|
||||
me={props.me}
|
||||
tabInfo={props.tabInfo}
|
||||
onTaskRowAction={props.onTaskRowAction} />;
|
||||
@@ -224,7 +244,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
return <FileBrowserTableRowNameCell
|
||||
treeRootData={props.treeRootData[props.selectedFolderData.group]}
|
||||
selectedFolderData={props.selectedFolderData}
|
||||
cellData={row}
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.name_text}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]} />;
|
||||
case "Size":
|
||||
return <TableRowSizeCell cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.metadata?.size}
|
||||
@@ -254,9 +274,9 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
return "showTask";
|
||||
}else if(sortedData.length > 0 || props?.selectedFolderData?.success){
|
||||
return "normal";
|
||||
} else if(sortedData.length === 0 && props?.selectedFolderData?.metadata?.has_children && props?.selectedFolderData?.success === null){
|
||||
} else if(sortedData.length === 0 && props?.selectedFolderData?.has_children && props?.selectedFolderData?.success === null){
|
||||
return "fetchLocal";
|
||||
}else if(sortedData.length === 0 && !props?.selectedFolderData?.metadata?.has_children && props?.selectedFolderData?.success === null) {
|
||||
}else if(sortedData.length === 0 && !props?.selectedFolderData?.has_children && props?.selectedFolderData?.success === null) {
|
||||
return "fetchRemote";
|
||||
}else if(sortedData.length === 0){
|
||||
return "fetchRemote";
|
||||
@@ -304,7 +324,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
};
|
||||
const contextMenuOptions = [
|
||||
{
|
||||
name: 'Filter Column',
|
||||
name: 'Filter Column', type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
@@ -319,18 +339,84 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Show/Hide Columns",
|
||||
name: "Reorder Columns and Adjust Visibility",type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
setOpenAdjustColumnsDialog(true);
|
||||
setOpenReorderDialog(true);
|
||||
}
|
||||
}
|
||||
];
|
||||
const onSubmitAdjustColumns = ({left, right}) => {
|
||||
setColumnVisibility({visible: right, hidden: left});
|
||||
React.useEffect( () => {
|
||||
try {
|
||||
const storageItem = GetMythicSetting({setting_name: `file_browser_table_columns`, default_value: defaultVisibleColumns});
|
||||
if(storageItem !== null){
|
||||
let allColumns = [...columnVisibility["visible"].map(c => c), ...columnVisibility["hidden"].map(c => c)];
|
||||
let newHidden = [];
|
||||
allColumns.forEach((v,i,a) => {
|
||||
if(!storageItem.includes(v)){
|
||||
newHidden.push(v);
|
||||
}
|
||||
});
|
||||
if(storageItem.length !== 0){
|
||||
setColumnVisibility({visible: storageItem, hidden: newHidden});
|
||||
}
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_columns", error);
|
||||
}
|
||||
try {
|
||||
const storageItemOptions = GetMythicSetting({setting_name: `file_browser_filter_options`, default_value: {}});
|
||||
if(storageItemOptions !== null){
|
||||
setFilterOptions(storageItemOptions);
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_filter_options", error);
|
||||
}
|
||||
try {
|
||||
const storageColumnOrder = GetMythicSetting({setting_name: `file_browser_column_order`, default_value: columns.map(c => c.name)});
|
||||
if(storageColumnOrder !== null){
|
||||
let newOrder = [];
|
||||
for(let i = 0; i < storageColumnOrder.length; i++){
|
||||
for(let j = 0; j < columnOrder.length; j++){
|
||||
if(columnOrder[j].name === storageColumnOrder[i]){
|
||||
newOrder.push(columnOrder[j]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
setColumnOrder(newOrder);
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load file_browser_table_filter_options", error);
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
const onSubmitColumnReorder = (newOrder) => {
|
||||
let newVisible = [];
|
||||
let newHidden = [];
|
||||
for(let i = 0; i < newOrder.length; i++){
|
||||
if(newOrder[i].visible){
|
||||
newVisible.push(newOrder[i].name);
|
||||
} else {
|
||||
newHidden.push(newOrder[i].name);
|
||||
}
|
||||
}
|
||||
if(newVisible.length === 0){
|
||||
snackActions.error("Can't update to show no fields");
|
||||
return;
|
||||
}
|
||||
console.log("newOrder", newOrder)
|
||||
updateSetting({setting_name: `file_browser_column_order`, value: newOrder.map(c => c.name)});
|
||||
setColumnOrder(newOrder);
|
||||
setColumnVisibility({visible: newVisible, hidden: newHidden});
|
||||
updateSetting({setting_name: `file_browser_table_columns`, value: newVisible});
|
||||
setOpenReorderDialog(false);
|
||||
}
|
||||
const onResetColumnReorder = () => {
|
||||
onSubmitColumnReorder(columnDefaults);
|
||||
}
|
||||
const sortColumn = columns.findIndex((column) => column.key === sortData.sortKey);
|
||||
const onRowClick = ({event, rowDataStatic}) => {
|
||||
@@ -365,7 +451,9 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
copyStringToClipboard(element.name_text);
|
||||
if(copyStringToClipboard(element.name_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -373,22 +461,26 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
copyStringToClipboard(element.full_path_text);
|
||||
if(copyStringToClipboard(element.full_path_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Attributes', type: "item",
|
||||
name: 'Metadata', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
copyStringToClipboard(JSON.stringify(element?.metadata?.permissions, null, 2));
|
||||
if(copyStringToClipboard(JSON.stringify(element?.metadata?.permissions, null, 2))){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
async function optionsB (callback_id, callback_display_id, element) {
|
||||
let downloadCommand = await props.getLoadedCommandForUIFeature(callback_id, element.can_have_children ? "file_browser:download_folder" : "file_browser:download");
|
||||
let downloadCommand = await props.getLoadedCommandForUIFeature(callback_id, element.can_have_children ? props.baseUIFeature + ":download_folder" : props.baseUIFeature + ":download");
|
||||
let downloadDisplay = "Download (Unsupported)";
|
||||
if(element.can_have_children){
|
||||
downloadDisplay = "Download Folder (Unsupported)";
|
||||
@@ -399,15 +491,21 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
downloadDisplay = `Download Folder (${downloadCommand.command.cmd})`;
|
||||
}
|
||||
}
|
||||
let listCommand = await props.getLoadedCommandForUIFeature(callback_id, "file_browser:list");
|
||||
let listCommand = await props.getLoadedCommandForUIFeature(callback_id, props.baseUIFeature + ":list");
|
||||
let listDisplay = "List (Unsupported)";
|
||||
if(listCommand !== undefined){
|
||||
listDisplay = `List (${listCommand.command.cmd})`;
|
||||
}
|
||||
let removeCommand = await props.getLoadedCommandForUIFeature(callback_id, "file_browser:remove");
|
||||
let removeCommand = await props.getLoadedCommandForUIFeature(callback_id, element.can_have_children ? props.baseUIFeature + ":remove_folder" : props.baseUIFeature + ":remove");
|
||||
let removeDisplay = "Remove (Unsupported)";
|
||||
if( element.can_have_children) {
|
||||
removeDisplay = "Remove Folder (Unsupported)"
|
||||
}
|
||||
if(removeCommand !== undefined){
|
||||
removeDisplay = `Remove (${removeCommand.command.cmd})`;
|
||||
if(element.can_have_children){
|
||||
removeDisplay = `Remove Folder (${removeCommand.command.cmd})`;
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
@@ -421,7 +519,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: element.can_have_children ? 'file_browser:download_folder' : 'file_browser:download',
|
||||
uifeature: element.can_have_children ? props.baseUIFeature + ':download_folder' : props.baseUIFeature + ':download',
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
},
|
||||
@@ -436,7 +534,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: 'file_browser:list',
|
||||
uifeature: props.baseUIFeature + ':list',
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
},
|
||||
@@ -451,7 +549,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: 'file_browser:remove',
|
||||
uifeature: element.can_have_children ? props.baseUIFeature + ':remove_folder' : props.baseUIFeature + ':remove',
|
||||
getConfirmation: true,
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
@@ -476,7 +574,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
full_path: selectedRows[i].full_path_text,
|
||||
host: selectedRows[i].host,
|
||||
filename: selectedRows[i].name_text,
|
||||
uifeature: selectedRows[i].can_have_children ? 'file_browser:download_folder': 'file_browser:download',
|
||||
uifeature: selectedRows[i].can_have_children ? props.baseUIFeature + ':download_folder': props.baseUIFeature + ':download',
|
||||
callback_id: props.tabInfo.callbackID,
|
||||
callback_display_id: props.tabInfo.displayID
|
||||
});
|
||||
@@ -496,7 +594,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
full_path: selectedRows[i].full_path_text,
|
||||
host: selectedRows[i].host,
|
||||
filename: selectedRows[i].name_text,
|
||||
uifeature: 'file_browser:remove',
|
||||
uifeature: props.baseUIFeature + ':remove',
|
||||
getConfirmation: true,
|
||||
callback_id: props.tabInfo.callbackID,
|
||||
callback_display_id: props.tabInfo.displayID
|
||||
@@ -517,7 +615,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
full_path: selectedRows[i].full_path_text,
|
||||
host: selectedRows[i].host,
|
||||
filename: selectedRows[i].name_text,
|
||||
uifeature: 'file_browser:list',
|
||||
uifeature: props.baseUIFeature + ':list',
|
||||
callback_id: props.tabInfo.callbackID,
|
||||
callback_display_id: props.tabInfo.displayID
|
||||
});
|
||||
@@ -539,6 +637,21 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
}
|
||||
return options;
|
||||
}
|
||||
if(loading){
|
||||
return (
|
||||
<div style={{width: '100%', height: '100%', position: "relative",}}>
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%"
|
||||
}}>
|
||||
{"Loading Saved Browser Customizations..."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div style={{width: '100%', height: '100%', overflow: "hidden", position: "relative"}}>
|
||||
{displayFormat === "normal" &&
|
||||
@@ -619,26 +732,34 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
<RenderSingleTask task_id={props.selectedFolderData?.task_id} />
|
||||
}
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xs" open={openContextMenu}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
innerDialog={<TableFilterDialog
|
||||
selectedColumn={selectedColumn}
|
||||
filterOptions={filterOptions}
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
onClose={()=>{setOpenContextMenu(false);}} />}
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={filterOptions[selectedColumn.key]}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
{openAdjustColumnsDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openAdjustColumnsDialog}
|
||||
onClose={()=>{setOpenAdjustColumnsDialog(false);}}
|
||||
innerDialog={
|
||||
<MythicTransferListDialog onClose={()=>{setOpenAdjustColumnsDialog(false);}}
|
||||
onSubmit={onSubmitAdjustColumns} right={columnVisibility.visible} rightTitle="Show these columns"
|
||||
leftTitle={"Hidden Columns"} left={columnVisibility.hidden} dialogTitle={"Edit which columns are shown"}/>}
|
||||
{openReorderDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openReorderDialog}
|
||||
onClose={()=>{setOpenReorderDialog(false);}}
|
||||
innerDialog={
|
||||
<CallbacksTableColumnsReorderDialog
|
||||
onClose={()=>{setOpenReorderDialog(false);}}
|
||||
visible={columnVisibility.visible}
|
||||
hidden={columnVisibility.hidden}
|
||||
onReset={onResetColumnReorder}
|
||||
onSubmit={onSubmitColumnReorder}
|
||||
initialItems={columnOrder}
|
||||
/>}
|
||||
/>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -646,8 +767,8 @@ const FileBrowserTableRowNameCell = ({cellData, rowData, treeRootData, selected
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div style={{ alignItems: 'center', display: 'flex', maxHeight: "100%", textDecoration: treeRootData[selectedFolderData.host][cellData]?.deleted ? 'line-through' : '' }}>
|
||||
{!treeRootData[selectedFolderData.host][cellData]?.can_have_children ? (
|
||||
<div style={{ alignItems: 'center', display: 'flex', maxHeight: "100%", textDecoration: treeRootData[selectedFolderData.host][rowData.full_path_text]?.deleted ? 'line-through' : '' }}>
|
||||
{!treeRootData[selectedFolderData.host][rowData.full_path_text]?.can_have_children ? (
|
||||
<DescriptionIcon size={"small"} style={{
|
||||
marginRight: '5px' }} />
|
||||
) : (
|
||||
@@ -658,7 +779,7 @@ const FileBrowserTableRowNameCell = ({cellData, rowData, treeRootData, selected
|
||||
marginRight: '8px',
|
||||
marginLeft: "4px",
|
||||
color:
|
||||
treeRootData[selectedFolderData.host][cellData]?.success || treeRootData[selectedFolderData.host][cellData]?.metadata?.has_children
|
||||
treeRootData[selectedFolderData.host][rowData.full_path_text]?.success || treeRootData[selectedFolderData.host][rowData.full_path_text]?.has_children
|
||||
? theme.folderColor
|
||||
: 'grey',
|
||||
}}
|
||||
@@ -667,17 +788,17 @@ const FileBrowserTableRowNameCell = ({cellData, rowData, treeRootData, selected
|
||||
<pre
|
||||
style={{
|
||||
color:
|
||||
treeRootData[selectedFolderData.host][cellData]?.success !== null
|
||||
treeRootData[selectedFolderData.host][rowData.full_path_text]?.success !== null
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary,
|
||||
}}>
|
||||
{treeRootData[selectedFolderData.host][cellData]?.name_text}
|
||||
{cellData}
|
||||
</pre>
|
||||
{treeRootData[selectedFolderData.host][cellData]?.success === true ? (
|
||||
{treeRootData[selectedFolderData.host][rowData.full_path_text]?.success === true ? (
|
||||
<MythicStyledTooltip title='Successfully listed contents of folder' tooltipStyle={{display: "inline-flex", marginLeft: "5px"}}>
|
||||
<CheckCircleOutlineIcon color="success" fontSize='small' />
|
||||
</MythicStyledTooltip>
|
||||
) : treeRootData[selectedFolderData.host][cellData]?.success === false ? (
|
||||
) : treeRootData[selectedFolderData.host][rowData.full_path_text]?.success === false ? (
|
||||
<MythicStyledTooltip title='Failed to list contents of folder' tooltipStyle={{display: "inline-flex", marginLeft: "5px"}}>
|
||||
<ErrorIcon fontSize='small' color="error" />
|
||||
</MythicStyledTooltip>
|
||||
@@ -810,7 +931,7 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
|
||||
};
|
||||
const optionsA = [
|
||||
{
|
||||
name: treeRootData[selectedFolderData.host][cellData]?.name_text, icon: null, click: ({event}) => {},
|
||||
name: treeRootData[selectedFolderData.host][rowData.full_path_text]?.name_text, icon: null, click: ({event}) => {},
|
||||
type: "item", disabled: true
|
||||
},
|
||||
{
|
||||
@@ -818,7 +939,7 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
|
||||
icon: <VisibilityIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
setPermissionData(treeRootData[selectedFolderData.host][cellData].metadata);
|
||||
setPermissionData(treeRootData[selectedFolderData.host][rowData.full_path_text].metadata);
|
||||
setViewPermissionsDialogOpen(true);
|
||||
},
|
||||
},
|
||||
@@ -828,7 +949,7 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
getHistory({ variables: {
|
||||
full_path_text: treeRootData[selectedFolderData.host][cellData].full_path_text,
|
||||
full_path_text: treeRootData[selectedFolderData.host][rowData.full_path_text].full_path_text,
|
||||
host: selectedFolderData.host,
|
||||
group: [selectedFolderData.group],
|
||||
} });
|
||||
@@ -867,8 +988,8 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
|
||||
ref={dropdownAnchorRef}>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
{treeRootData[selectedFolderData.host][cellData]?.filemeta.length > 0 ?
|
||||
<MythicStyledTooltip title={treeRootData[selectedFolderData.host][cellData]?.filemeta[0]?.complete ?
|
||||
{treeRootData[selectedFolderData.host][rowData.full_path_text]?.filemeta.length > 0 ?
|
||||
<MythicStyledTooltip title={treeRootData[selectedFolderData.host][rowData.full_path_text]?.filemeta[0]?.complete ?
|
||||
"Preview Media" : "Preview Partial Media"}>
|
||||
<FontAwesomeIcon icon={faPhotoVideo} size={"1x"} style={{
|
||||
position: "relative", cursor: "pointer", display: "inline-block"}}
|
||||
@@ -880,8 +1001,8 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openPreviewMediaDialog}
|
||||
onClose={(e)=>{setOpenPreviewMediaDialog(false);}}
|
||||
innerDialog={<PreviewFileMediaDialog
|
||||
agent_file_id={treeRootData[selectedFolderData.host][cellData]?.filemeta[0]?.agent_file_id}
|
||||
filename={b64DecodeUnicode(treeRootData[selectedFolderData.host][cellData]?.filemeta[0]?.filename_text)}
|
||||
agent_file_id={treeRootData[selectedFolderData.host][rowData.full_path_text]?.filemeta[0]?.agent_file_id}
|
||||
filename={b64DecodeUnicode(treeRootData[selectedFolderData.host][rowData.full_path_text]?.filemeta[0]?.filename_text)}
|
||||
onClose={(e)=>{setOpenPreviewMediaDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -6,11 +6,14 @@ import ListIcon from '@mui/icons-material/List';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import {getAllParentNodes} from "./CallbacksTabsFileBrowser";
|
||||
import {Dropdown, DropdownMenuItem, DropdownNestedMenuItem} from "../../MythicComponents/MythicNestedMenus";
|
||||
import {copyStringToClipboard} from "../../utilities/Clipboard";
|
||||
import FileCopyOutlinedIcon from '@mui/icons-material/FileCopyOutlined';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
|
||||
export const getOpenIDFromNode = (node) => {
|
||||
return `${node.group};${node.host};${node.full_path_text}`;
|
||||
}
|
||||
export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatrix, fetchFolderData, setTableData, taskListing, tableOpenedPathId, showDeletedFiles, tabInfo, selectedFolderData}) => {
|
||||
export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatrix, fetchFolderData, setTableData, taskListing, tableOpenedPathId, showDeletedFiles, tabInfo, selectedFolderData, baseUIFeature}) => {
|
||||
const [openNodes, setOpenNodes] = React.useState({});
|
||||
const groupName = React.useRef("");
|
||||
const [openViewGroupsDialog, setOpenViewGroupDialog] = React.useState(false);
|
||||
@@ -19,7 +22,7 @@ export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatri
|
||||
//console.log("toggleNodeExpanded", nodeId, nodeData);
|
||||
let lastOpenedNodeNewState = true;
|
||||
let lastOpenedNodeID = getOpenIDFromNode(lastOpenedNodeRef.current);
|
||||
if(!lastOpenedNodeRef.current?.metadata?.has_children){
|
||||
if(!lastOpenedNodeRef.current?.has_children){
|
||||
lastOpenedNodeNewState = false;
|
||||
}
|
||||
lastOpenedNodeRef.current = {...nodeData};
|
||||
@@ -28,7 +31,7 @@ export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatri
|
||||
setOpenNodes({
|
||||
...openNodes,
|
||||
[getOpenIDFromNode(nodeData)]: true,
|
||||
[lastOpenedNodeID]: lastOpenedNodeNewState
|
||||
//[lastOpenedNodeID]: lastOpenedNodeNewState
|
||||
});
|
||||
};
|
||||
const toggleNodeCollapsed = (nodeId, nodeData) => {
|
||||
@@ -77,6 +80,41 @@ export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatri
|
||||
});
|
||||
}, [selectedFolderData]);
|
||||
const contextMenuOptions= (callback_id, callback_display_id, node) => [
|
||||
{
|
||||
name: "Copy to Clipboard", icon: null, click: () => {}, type: "menu",
|
||||
menuItems: [
|
||||
{
|
||||
name: 'Name', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(node.name_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Full Path', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(node.full_path_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Metadata', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(JSON.stringify(node?.metadata, null, 2))){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'List', type: "item", icon: <ListIcon color="warning" style={{ paddingRight: '5px'}} />,
|
||||
click: ({event}) => {
|
||||
@@ -146,6 +184,7 @@ export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatri
|
||||
onCollapseNode={toggleNodeCollapsed}
|
||||
onContextMenu={onContextMenu}
|
||||
tabInfo={tabInfo}
|
||||
baseUIFeature={baseUIFeature}
|
||||
/>
|
||||
{openViewGroupsDialog &&
|
||||
<MythicDialog
|
||||
|
||||
@@ -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 DialogContentText from '@mui/material/DialogContentText';
|
||||
|
||||
|
||||
const callbacksAndFeaturesQuery = gql`
|
||||
@@ -175,6 +176,9 @@ export function CallbacksTabsHideMultipleDialog({onClose}) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Hide Multiple Callbacks at Once</DialogTitle>
|
||||
<DialogContentText style={{textAlign: "center"}}>
|
||||
<b>Note: </b> Last checkin times are based on when this window opened and won't refresh.
|
||||
</DialogContentText>
|
||||
<CustomSelectTable initialData={initialData}
|
||||
selectedData={selectedData}
|
||||
sortModel={{ field: 'last_checkin', sort: 'asc' }}
|
||||
@@ -217,6 +221,9 @@ export function CallbacksTabsSelectMultipleDialog({onClose, onSubmit}) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Select Multiple Callbacks</DialogTitle>
|
||||
<DialogContentText style={{textAlign: "center"}}>
|
||||
<b>Note: </b> Last checkin times are based on when this window opened and won't refresh.
|
||||
</DialogContentText>
|
||||
<CustomSelectTable initialData={initialData}
|
||||
selectedData={selectedData}
|
||||
sortModel={{ field: 'display_id', sort: 'desc' }}
|
||||
|
||||
@@ -5,6 +5,7 @@ import DialogTitle from '@mui/material/DialogTitle';
|
||||
import {useQuery, gql } from '@apollo/client';
|
||||
import {CallbacksTableLastCheckinCell, CallbacksTablePayloadTypeCell, CallbacksTableIPCell} from "./CallbacksTableRow";
|
||||
import { DataGrid } from '@mui/x-data-grid';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
|
||||
|
||||
const callbacksAndFeaturesQuery = gql`
|
||||
@@ -140,6 +141,9 @@ export function CallbacksTabsOpenMultipleDialog({onClose, tabType, onOpenTabs})
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Select Callback to open {tabType} tab</DialogTitle>
|
||||
<DialogContentText style={{textAlign: "center"}}>
|
||||
<b>Note: </b> Last checkin times are based on when this window opened and won't refresh.
|
||||
</DialogContentText>
|
||||
<CustomSelectTable initialData={initialData}
|
||||
selectedData={selectedData}
|
||||
onRowClick={onRowClick}
|
||||
|
||||
@@ -6,8 +6,6 @@ import {useTheme} from '@mui/material/styles';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import {CallbacksTabsProcessBrowserTable} from './CallbacksTabsProcessBrowserTable';
|
||||
import {MythicModifyStringDialog} from '../../MythicComponents/MythicDialog';
|
||||
import {TaskFromUIButton} from './TaskFromUIButton';
|
||||
@@ -271,11 +269,14 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me, set
|
||||
treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]].callbacks = [data.data.mythictree_stream[i].callback]
|
||||
} else {
|
||||
// we need to merge data in because we already have some info
|
||||
if(data.data.mythictree_stream[i].deleted){
|
||||
delete treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]];
|
||||
continue;
|
||||
}
|
||||
let existingData = treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]];
|
||||
if(existingData.task_id <= data.data.mythictree_stream[i].task_id){
|
||||
if(data.data.mythictree_stream[i].deleted){
|
||||
delete treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
existingData.has_children = data.data.mythictree_stream[i].has_children || existingData.has_children;
|
||||
existingData.callbacks.push(data.data.mythictree_stream[i].callback)
|
||||
existingData.comment += data.data.mythictree_stream[i].comment;
|
||||
existingData.tags = [...existingData.tags, ...data.data.mythictree_stream[i].tags];
|
||||
|
||||
@@ -9,8 +9,6 @@ import ListIcon from '@mui/icons-material/List';
|
||||
import { snackActions } from '../../utilities/Snackbar';
|
||||
import 'react-virtualized/styles.css';
|
||||
import MythicResizableGrid from '../../MythicComponents/MythicResizableGrid';
|
||||
import {TableFilterDialog} from './TableFilterDialog';
|
||||
import {MythicTransferListDialog} from '../../MythicComponents/MythicTransferList';
|
||||
import {TagsDisplay, ViewEditTags} from '../../MythicComponents/MythicTag';
|
||||
import TerminalIcon from '@mui/icons-material/Terminal';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
@@ -22,7 +20,12 @@ import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
import {Dropdown, DropdownMenuItem, DropdownNestedMenuItem} from "../../MythicComponents/MythicNestedMenus";
|
||||
import {faSkullCrossbones, faSyringe, faKey,} from '@fortawesome/free-solid-svg-icons';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {GetComputedFontSize} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {
|
||||
GetComputedFontSize,
|
||||
GetMythicSetting,
|
||||
useSetMythicSetting
|
||||
} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {CallbacksTableColumnsReorderDialog} from "./CallbacksTableColumnsReorderDialog";
|
||||
|
||||
const getPermissionsDataQuery = gql`
|
||||
query getPermissionsQuery($mythictree_id: Int!) {
|
||||
@@ -40,24 +43,40 @@ const updateFileComment = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const columnDefaults = [
|
||||
{ name: 'Info', width: 50, disableAutosize: true, disableSort: true, disableFilterMenu: true, key: "info", visible: true },
|
||||
{ name: 'PID', type: 'number', key: 'process_id', inMetadata: true, width: 100, visible: true},
|
||||
{ name: 'PPID', type: 'number', key: 'parent_process_id', inMetadata: true, width: 100, visible: true},
|
||||
{ name: 'Name', type: 'string', disableSort: false, key: 'name_text', fillWidth: true, visible: true },
|
||||
{ name: "Arch", type: 'string', key: 'architecture', inMetadata: true, width: 70, visible: true},
|
||||
{ name: "Integrity", type: "number", key: "integrity_level", inMetadata: true, width: 100, visible: false },
|
||||
{ name: 'Session', type: 'number', key: 'session_id', inMetadata: true, width: 100, visible: true},
|
||||
{ name: "User", type: 'string', key: 'user', inMetadata: true, fillWidth: true, visible: true},
|
||||
{ name: 'Tags', type: 'tags', disableSort: true, disableFilterMenu: true, width: 220, key: "tags", visible: false },
|
||||
{ name: 'Comment', type: 'string', key: 'comment', disableSort: false, width: 200, visible: false },
|
||||
{ name: "CMD", type: "string", key: 'command_line', inMetadata: true, fillWidth: true, visible: true},
|
||||
];
|
||||
const defaultVisibleColumns = ["Info","PID", "PPID", "Name", "Arch", "Session", "User", "CMD"];
|
||||
export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, me, onRowDoubleClick,
|
||||
onTaskRowAction, host, group, showDeletedFiles, tabInfo,
|
||||
expandOrCollapseAll, getLoadedCommandForUIFeature}) => {
|
||||
//const [allData, setAllData] = React.useState([]);
|
||||
//console.log("treeAdjMatrix updated in table", treeAdjMatrix)
|
||||
const [updateSetting] = useSetMythicSetting();
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [sortData, setSortData] = React.useState({"sortKey": null, "sortDirection": null, "sortType": null});
|
||||
const [openNodes, setOpenNodes] = React.useState({});
|
||||
const [openContextMenu, setOpenContextMenu] = React.useState(false);
|
||||
const [filterOptions, setFilterOptions] = React.useState({});
|
||||
const selectedColumn = React.useRef({});
|
||||
const [columnVisibility, setColumnVisibility] = React.useState({
|
||||
"visible": ["Info","PID", "PPID", "Name", "Arch", "Session", "User", "CMD"],
|
||||
"visible": defaultVisibleColumns,
|
||||
"hidden": [ "Comment", "Tags" ]
|
||||
})
|
||||
const [singleTreeData, setSingleTreeData] = React.useState({});
|
||||
const [viewSingleTreeData, setViewSingleTreeData] = React.useState(false);
|
||||
const [openAdjustColumnsDialog, setOpenAdjustColumnsDialog] = React.useState(false);
|
||||
const [openReorderDialog, setOpenReorderDialog] = React.useState(false);
|
||||
const [columnOrder, setColumnOrder] = React.useState(columnDefaults);
|
||||
const [updatedTreeAdjMatrix, setUpdatedTreeAdjMatrix] = React.useState(treeAdjMatrix);
|
||||
const openAllNodes = (state) => {
|
||||
let onodes = {};
|
||||
@@ -192,21 +211,10 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
// }
|
||||
|
||||
};
|
||||
const columnDefaults = [
|
||||
{ name: 'Info', width: 50, disableAutosize: true, disableSort: true, disableFilterMenu: true },
|
||||
{ name: 'PID', type: 'number', key: 'process_id', inMetadata: true, width: 100},
|
||||
{ name: 'PPID', type: 'number', key: 'parent_process_id', inMetadata: true, width: 100},
|
||||
{ name: 'Name', type: 'string', disableSort: false, key: 'name_text', fillWidth: true },
|
||||
{ name: "Arch", type: 'string', key: 'architecture', inMetadata: true, width: 70},
|
||||
{ name: 'Session', type: 'number', key: 'session_id', inMetadata: true, width: 100},
|
||||
{ name: "User", type: 'string', key: 'user', inMetadata: true, fillWidth: true},
|
||||
{ name: 'Tags', type: 'tags', disableSort: true, disableFilterMenu: true, width: 220 },
|
||||
{ name: 'Comment', type: 'string', key: 'comment', disableSort: false, width: 200 },
|
||||
{ name: "CMD", type: "string", key: 'command_line', inMetadata: true, fillWidth: true},
|
||||
];
|
||||
|
||||
const columns = React.useMemo(
|
||||
() =>
|
||||
columnDefaults.reduce( (prev, cur) => {
|
||||
() =>
|
||||
columnOrder.reduce( (prev, cur) => {
|
||||
if(columnVisibility.visible.includes(cur.name)){
|
||||
if(filterOptions[cur.key] && String(filterOptions[cur.key]).length > 0){
|
||||
return [...prev, {...cur, filtered: true}];
|
||||
@@ -217,7 +225,7 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
return [...prev];
|
||||
}
|
||||
}, [])
|
||||
, [filterOptions, columnVisibility]
|
||||
, [filterOptions, columnVisibility, columnOrder]
|
||||
);
|
||||
const flattenNode = useCallback(
|
||||
(node, host, group, depth = 0) => {
|
||||
@@ -384,8 +392,14 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
}
|
||||
return tempData;
|
||||
}, [allData, sortData]);
|
||||
const onSubmitFilterOptions = (newFilterOptions) => {
|
||||
setFilterOptions(newFilterOptions);
|
||||
const onSubmitFilterOptions = (value) => {
|
||||
let newValue = value ? value : "";
|
||||
try{
|
||||
setFilterOptions({...filterOptions, [selectedColumn.current.key]: newValue });
|
||||
updateSetting({setting_name: `process_browser_filter_options`, value: {...filterOptions,[selectedColumn.current.key]: newValue }});
|
||||
}catch(error){
|
||||
console.log("failed to save filter options", error, filterOptions, selectedColumn.current.key, newValue);
|
||||
}
|
||||
if(viewSingleTreeData){
|
||||
return
|
||||
}
|
||||
@@ -482,7 +496,7 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
}
|
||||
|
||||
setSingleTreeData(singleTreeAdjMatrix);
|
||||
onSubmitFilterOptions({});
|
||||
onSubmitFilterOptions(undefined);
|
||||
}
|
||||
const toggleViewSingleTreeData = () => {
|
||||
setViewSingleTreeData(!viewSingleTreeData);
|
||||
@@ -550,6 +564,13 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
group={group}
|
||||
rowData={row}
|
||||
cellData={treeRootData[group][host][row.full_path_text]?.parent_path_text || ""} />;
|
||||
case "Integrity":
|
||||
return <FileBrowserTableRowStringCell
|
||||
treeRootData={treeRootData[group]}
|
||||
host={host}
|
||||
group={group}
|
||||
rowData={row}
|
||||
cellData={treeRootData[group][host][row.full_path_text ]?.metadata?.integrity_level || ''} />;
|
||||
case "Tags":
|
||||
return <FileBrowserTagsCell
|
||||
rowData={row}
|
||||
@@ -574,7 +595,7 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
cellData={treeRootData[group][host][row.full_path_text ]?.metadata?.command_line || ""}
|
||||
/>;
|
||||
default:
|
||||
console.log("hit default case in switch on c.name)")
|
||||
console.log("hit default case in switch on c.name)", c.name)
|
||||
}
|
||||
})];
|
||||
}
|
||||
@@ -605,7 +626,7 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
};
|
||||
const contextMenuOptions = [
|
||||
{
|
||||
name: 'Filter Column',
|
||||
name: 'Filter Column', type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
@@ -620,7 +641,7 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Show/Hide Columns",
|
||||
name: "Reorder Columns and Adjust Visibility", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
@@ -630,12 +651,77 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
snackActions.warning("Can't filter that column");
|
||||
return;
|
||||
}
|
||||
setOpenAdjustColumnsDialog(true);
|
||||
setOpenReorderDialog(true);
|
||||
}
|
||||
}
|
||||
];
|
||||
const onSubmitAdjustColumns = ({left, right}) => {
|
||||
setColumnVisibility({visible: right, hidden: left});
|
||||
React.useEffect( () => {
|
||||
try {
|
||||
const storageItem = GetMythicSetting({setting_name: `process_browser_table_columns`, default_value: defaultVisibleColumns});
|
||||
if(storageItem !== null){
|
||||
let allColumns = [...columnVisibility["visible"].map(c => c), ...columnVisibility["hidden"].map(c => c)];
|
||||
let newHidden = [];
|
||||
allColumns.forEach((v,i,a) => {
|
||||
if(!storageItem.includes(v)){
|
||||
newHidden.push(v);
|
||||
}
|
||||
});
|
||||
if(storageItem.length !== 0){
|
||||
setColumnVisibility({visible: storageItem, hidden: newHidden});
|
||||
}
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_columns", error);
|
||||
}
|
||||
try {
|
||||
const storageItemOptions = GetMythicSetting({setting_name: `process_browser_filter_options`, default_value: {}});
|
||||
if(storageItemOptions !== null){
|
||||
setFilterOptions(storageItemOptions);
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_filter_options", error);
|
||||
}
|
||||
try {
|
||||
const storageColumnOrder = GetMythicSetting({setting_name: `process_browser_column_order`, default_value: columns.map(c => c.name)});
|
||||
if(storageColumnOrder !== null){
|
||||
let newOrder = [];
|
||||
for(let i = 0; i < storageColumnOrder.length; i++){
|
||||
for(let j = 0; j < columnOrder.length; j++){
|
||||
if(columnOrder[j].name === storageColumnOrder[i]){
|
||||
newOrder.push(columnOrder[j]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
setColumnOrder(newOrder);
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load process_browser_table_filter_options", error);
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
const onSubmitColumnReorder = (newOrder) => {
|
||||
let newVisible = [];
|
||||
let newHidden = [];
|
||||
for(let i = 0; i < newOrder.length; i++){
|
||||
if(newOrder[i].visible){
|
||||
newVisible.push(newOrder[i].name);
|
||||
} else {
|
||||
newHidden.push(newOrder[i].name);
|
||||
}
|
||||
}
|
||||
if(newVisible.length === 0){
|
||||
snackActions.error("Can't update to show no fields");
|
||||
return;
|
||||
}
|
||||
updateSetting({setting_name: `process_browser_column_order`, value: newOrder.map(c => c.name)});
|
||||
setColumnOrder(newOrder);
|
||||
setColumnVisibility({visible: newVisible, hidden: newHidden});
|
||||
updateSetting({setting_name: `process_browser_table_columns`, value: newVisible});
|
||||
setOpenReorderDialog(false);
|
||||
}
|
||||
const onResetColumnReorder = () => {
|
||||
onSubmitColumnReorder(columnDefaults);
|
||||
}
|
||||
const sortColumn = columns.findIndex((column) => column.key === sortData.sortKey);
|
||||
React.useEffect( () => {
|
||||
@@ -648,6 +734,21 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
openAllNodes(false);
|
||||
}
|
||||
}, [expandOrCollapseAll, updatedTreeAdjMatrix]);
|
||||
if(loading){
|
||||
return (
|
||||
<div style={{width: '100%', height: '100%', position: "relative",}}>
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%"
|
||||
}}>
|
||||
{"Loading Saved Browser Customizations..."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%', overflow: "hidden", position: "relative" }}>
|
||||
<MythicResizableGrid
|
||||
@@ -661,24 +762,34 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
/>
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xs" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={<TableFilterDialog
|
||||
selectedColumn={selectedColumn.current}
|
||||
filterOptions={filterOptions}
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
onClose={()=>{setOpenContextMenu(false);}} />}
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={String(filterOptions[selectedColumn.current.key])}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
{openAdjustColumnsDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openAdjustColumnsDialog}
|
||||
onClose={()=>{setOpenAdjustColumnsDialog(false);}}
|
||||
innerDialog={
|
||||
<MythicTransferListDialog onClose={()=>{setOpenAdjustColumnsDialog(false);}}
|
||||
onSubmit={onSubmitAdjustColumns} right={columnVisibility.visible} rightTitle="Show these columns"
|
||||
leftTitle={"Hidden Columns"} left={columnVisibility.hidden} dialogTitle={"Edit which columns are shown"}/>}
|
||||
{openReorderDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openReorderDialog}
|
||||
onClose={()=>{setOpenReorderDialog(false);}}
|
||||
innerDialog={
|
||||
<CallbacksTableColumnsReorderDialog
|
||||
onClose={()=>{setOpenReorderDialog(false);}}
|
||||
visible={columnVisibility.visible}
|
||||
hidden={columnVisibility.hidden}
|
||||
onReset={onResetColumnReorder}
|
||||
onSubmit={onSubmitColumnReorder}
|
||||
initialItems={columnOrder}
|
||||
/>}
|
||||
/>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {CallbacksTableIPCell, CallbacksTableLastCheckinCell} from "./CallbacksTa
|
||||
import { DataGrid } from '@mui/x-data-grid';
|
||||
import { validate as uuidValidate } from 'uuid';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
|
||||
|
||||
const callbacksAndFeaturesQuery = gql`
|
||||
@@ -291,12 +292,16 @@ export function CallbacksTabsTaskMultipleDialog({onClose, callback}) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Task Multiple {callback.payload.payloadtype.name} Callbacks at Once</DialogTitle>
|
||||
<DialogContentText style={{textAlign: "center"}}>
|
||||
<b>Note: </b> Last checkin times are based on when this window opened and won't refresh.
|
||||
</DialogContentText>
|
||||
<CustomSelectTable initialData={initialData}
|
||||
selectedData={selectedData} />
|
||||
<Grid size={12}>
|
||||
<CallbacksTabsTaskingInput filterTasks={false} onSubmitFilter={()=>{}} onSubmitCommandLine={onSubmitCommandLine}
|
||||
changeSelectedToken={changeSelectedToken}
|
||||
payloadtype_name={callback.payload.payloadtype.name}
|
||||
hide_context={true}
|
||||
filterOptions={{}} callback_id={callback.id} callback_os={callback.payload.os} parentMountedRef={mountedRef} />
|
||||
</Grid>
|
||||
{openTaskingButton &&
|
||||
|
||||
@@ -264,6 +264,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
const me = useReactiveVar(meState);
|
||||
const useDisplayParamsForCLIHistoryUserSetting = React.useRef(GetMythicSetting({setting_name: "useDisplayParamsForCLIHistory", default_value: operatorSettingDefaults.useDisplayParamsForCLIHistory}));
|
||||
const hideTaskingContext = React.useRef(GetMythicSetting({setting_name: "hideTaskingContext", default_value: operatorSettingDefaults.hideTaskingContext}));
|
||||
hideTaskingContext.current = props.hide_context | hideTaskingContext.current;
|
||||
const taskingContextFields = React.useRef(GetMythicSetting({setting_name: "taskingContextFields", default_value: operatorSettingDefaults.taskingContextFields}));
|
||||
const forwardOrBackwardTabIndex = (event, currentIndex, options) => {
|
||||
if(event.shiftKey){
|
||||
@@ -284,6 +285,9 @@ export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
return;
|
||||
}
|
||||
tokenOptions.current = data.data.callbacktoken;
|
||||
if(tokenOptions.current.length === 0) {
|
||||
props.changeSelectedToken("Default Token");
|
||||
}
|
||||
}
|
||||
});
|
||||
useSubscription(contextSubscription, {
|
||||
@@ -789,7 +793,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
str.split('').forEach((value, i, s) => {
|
||||
//loop over every value in the string
|
||||
//console.log(value);
|
||||
if(value === "\\"){
|
||||
if( (sQuoted || dQuoted) && value === "\\"){
|
||||
if(!backslash){
|
||||
backslash = true;
|
||||
return;
|
||||
@@ -823,7 +827,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
buffer += value;
|
||||
return;
|
||||
}
|
||||
else if(value === " "){
|
||||
else if([" ", "\t", "\r", "\n"].includes(value)){
|
||||
if(backslash){
|
||||
backslash = false;
|
||||
//buffer += " ";
|
||||
@@ -1794,11 +1798,11 @@ export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
innerDialog={<MythicSelectFromListDialog onClose={() => {
|
||||
setOpenSelectCommandDialog(false);
|
||||
}}
|
||||
onSubmit={processCommandAndCommandLine}
|
||||
options={commandOptions.current}
|
||||
title={"Select Command"}
|
||||
action={"select"} identifier={"id"}
|
||||
display={"display"}/>}
|
||||
onSubmit={processCommandAndCommandLine}
|
||||
options={commandOptions.current}
|
||||
title={"Select Command"}
|
||||
action={"select"} identifier={"id"}
|
||||
display={"display"}/>}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ export function CallbacksTabsTaskingInputTokenSelect(props) {
|
||||
}
|
||||
}
|
||||
return (
|
||||
<FormControl style={{width: "20%"}}>
|
||||
<FormControl style={{width: props.width ? props.width : "20%"}}>
|
||||
<Select
|
||||
labelId="demo-dialog-select-label"
|
||||
id="demo-dialog-select"
|
||||
|
||||
@@ -377,7 +377,7 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
|
||||
}
|
||||
}
|
||||
const changeSelectedTask = (task) => {
|
||||
let updated = {...taskingData};
|
||||
let updated = {...taskingDataRef.current};
|
||||
for(let i = 0; i < updated.task.length; i++){
|
||||
if(updated.task[i].id === task.id){
|
||||
updated.task[i].selected = true;
|
||||
@@ -414,7 +414,8 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
|
||||
}, [taskingData, newlyIssuedTasks]);
|
||||
return (
|
||||
<MythicTabPanel index={index} value={value} >
|
||||
<Split direction="horizontal" minSize={[0,0]} style={{width: "100%", height: "100%", overflowY: "auto", display: "flex"}} sizes={[30, 70]} >
|
||||
<Split direction="horizontal" minSize={[0,0]} sizes={[30, 70]}
|
||||
style={{width: "100%", height: "100%", overflow: "auto", display: "flex", justifyContent: "flex-end"}} >
|
||||
<div className="bg-gray-base" style={{display: "inline-flex", flexDirection: "column"}} >
|
||||
|
||||
<div style={{overflowY: "auto", flexGrow: 1}} id={`taskingPanelSplit${tabInfo.callbackID}`}>
|
||||
@@ -446,7 +447,14 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="bg-gray-light" style={{display: "inline-flex", height: "100%"}}>
|
||||
<div className="bg-gray-light" style={{display: "inline-flex", flexDirection: "column", height: "100%", width: "100%"}}>
|
||||
{selectedTask.id > 0 && <TaskDisplayFlat key={"taskinteractdisplaysplit" + selectedTask.id} me={me} task={selectedTask}
|
||||
command_id={selectedTask.command == null ? 0 : selectedTask.command.id}
|
||||
filterOptions={filterOptions}
|
||||
onSelectTask={(tsk) => {
|
||||
}}
|
||||
selectedTask={selectedTask}
|
||||
/>}
|
||||
<CallbacksTabsTaskingSplitTable selectedTask={selectedTask} me={me} filterOptions={filterOptions}
|
||||
onSelectTask={(tsk) => {changeSelectedTask(tsk)}} changeSelectedTask={changeSelectedTask}
|
||||
/>
|
||||
|
||||
@@ -384,7 +384,7 @@ export function CallbacksTop(props){
|
||||
callbackEdges.current = updated;
|
||||
}
|
||||
});
|
||||
const onOpenTabLocal = React.useCallback( ({tabType, tabID, callbackID}) => {
|
||||
const onOpenTabLocal = React.useCallback( ({tabType, tabID, callbackID, customBrowser}) => {
|
||||
for(let i = 0; i < callbacks.current.length; i++){
|
||||
if(callbacks.current[i]["id"] === callbackID){
|
||||
const tabData = {tabID, tabType, callbackID,
|
||||
@@ -396,7 +396,9 @@ export function CallbacksTop(props){
|
||||
callbackDescription: callbacks.current[i]["description"],
|
||||
host: callbacks.current[i]["host"],
|
||||
color: callbacks.current[i]["color"],
|
||||
os: callbacks.current[i]["payload"]["os"]};
|
||||
os: callbacks.current[i]["payload"]["os"],
|
||||
customBrowser: customBrowser,
|
||||
};
|
||||
props.onOpenTab(tabData);
|
||||
}
|
||||
}
|
||||
@@ -418,7 +420,9 @@ export function CallbacksTop(props){
|
||||
callbackDescription: callbacks.current[i]["description"],
|
||||
host: callbacks.current[i]["host"],
|
||||
color: callbacks.current[i]["color"],
|
||||
os: callbacks.current[i]["payload"]["os"]};
|
||||
os: callbacks.current[i]["payload"]["os"],
|
||||
customBrowser: tabs[j].customBrowser,
|
||||
};
|
||||
newTabData.push(tabData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ export function DownloadHistoryDialog(props){
|
||||
<TableRow>
|
||||
<TableCell style={{}}></TableCell>
|
||||
<TableCell style={{width: "15rem"}}>Time</TableCell>
|
||||
<TableCell style={{width: "5rem"}}>Task</TableCell>
|
||||
<TableCell style={{width: "5rem"}}>Callback</TableCell>
|
||||
<TableCell style={{width: "7rem"}}>Task</TableCell>
|
||||
<TableCell style={{width: "7rem"}}>Callback</TableCell>
|
||||
<TableCell>Comment</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
@@ -16,6 +16,8 @@ import {ResponseDisplayInteractive} from "./ResponseDisplayInteractive";
|
||||
import {ResponseDisplayMedia} from "./ResponseDisplayMedia";
|
||||
import {GetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {ResponseDisplayGraph} from "./ResponseDisplayGraph";
|
||||
import {operatorSettingDefaults} from "../../../cache";
|
||||
import {ResponseDisplayTabs} from "./ResponseDisplayTabs";
|
||||
|
||||
const subResponsesStream = gql`
|
||||
subscription subResponsesStream($task_id: Int!){
|
||||
@@ -95,13 +97,14 @@ export const ResponseDisplay = (props) =>{
|
||||
const interactive = props?.task?.command?.supported_ui_features.includes("task_response:interactive") || false;
|
||||
return (
|
||||
interactive ? (
|
||||
<ResponseDisplayInteractive {...props} />
|
||||
<ResponseDisplayInteractive {...props} key={props?.task?.id} />
|
||||
) : (
|
||||
<NonInteractiveResponseDisplay {...props} />
|
||||
<NonInteractiveResponseDisplay {...props} key={props?.task?.id} />
|
||||
)
|
||||
)
|
||||
}
|
||||
const NonInteractiveResponseDisplay = (props) => {
|
||||
const theme = useTheme();
|
||||
const [output, setOutput] = React.useState("");
|
||||
const [rawResponses, setRawResponses] = React.useState([]);
|
||||
const seenResponseIDs = React.useRef([]);
|
||||
@@ -109,7 +112,7 @@ const NonInteractiveResponseDisplay = (props) => {
|
||||
const [totalCount, setTotalCount] = React.useState(0);
|
||||
const [openBackdrop, setOpenBackdrop] = React.useState(true);
|
||||
const togglingAllOutputToPaginated = React.useRef(false);
|
||||
const initialResponseStreamLimit = GetMythicSetting({setting_name: "experiment-responseStreamLimit", default_value: 50});
|
||||
const initialResponseStreamLimit = GetMythicSetting({setting_name: "experiment-responseStreamLimit", default_value: operatorSettingDefaults["experiment-responseStreamLimit"]});
|
||||
const [fetchMoreResponses] = useLazyQuery(getResponsesLazyQuery, {
|
||||
fetchPolicy: "network-only",
|
||||
onCompleted: (data) => {
|
||||
@@ -289,16 +292,19 @@ const NonInteractiveResponseDisplay = (props) => {
|
||||
}, [search.current]);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<Backdrop open={openBackdrop} onClick={()=>{setOpenBackdrop(false);}} style={{zIndex: 2, position: "absolute"}}>
|
||||
<CircularProgress color="inherit" disableShrink />
|
||||
</Backdrop>
|
||||
{props.searchOutput &&
|
||||
<SearchBar onSubmitSearch={onSubmitSearch} />
|
||||
}
|
||||
{!openBackdrop &&
|
||||
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%"}}>
|
||||
|
||||
{!openBackdrop &&
|
||||
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%",
|
||||
backgroundColor: theme.outputBackgroundColor + (theme.palette.mode === 'dark' ? "D0" : "D0"),
|
||||
color: theme.outputTextColor,
|
||||
}}>
|
||||
{props.searchOutput &&
|
||||
<SearchBar onSubmitSearch={onSubmitSearch} />
|
||||
}
|
||||
<div style={{overflowY: "auto", flexGrow: 1, width: "100%", height: props.expand ? "100%": undefined, display: "flex", flexDirection: "column"}} ref={props.responseRef}>
|
||||
<ResponseDisplayComponent rawResponses={rawResponses} viewBrowserScript={props.viewBrowserScript}
|
||||
output={output} command_id={props.command_id} displayType={"accordion"}
|
||||
@@ -312,7 +318,7 @@ const NonInteractiveResponseDisplay = (props) => {
|
||||
</div>
|
||||
|
||||
}
|
||||
</React.Fragment>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export const ResponseDisplayConsole = (props) => {
|
||||
@@ -402,7 +408,7 @@ export const PaginationBar = ({selectAllOutput, totalCount, onSubmitPageChange,
|
||||
}, [totalCount, maxCount, search, selectAllOutput]);
|
||||
const pageCount = Math.max(1, Math.ceil(localTotalCount / pageSize));
|
||||
// don't bother people with pagination information if they haven't even started paginating
|
||||
if(pageCount < 2){
|
||||
if(pageCount < 2 || pageCount === Infinity || isNaN(pageCount)){
|
||||
return (<div id={'scrolltotaskbottom' + task.id}></div>)
|
||||
}
|
||||
return (
|
||||
@@ -540,47 +546,64 @@ const ResponseDisplayComponent = ({rawResponses, viewBrowserScript, output, comm
|
||||
return null
|
||||
}
|
||||
return (
|
||||
localViewBrowserScript && Object.keys(browserScriptData).length > 0 ? (
|
||||
<React.Fragment>
|
||||
{browserScriptData?.screenshot?.map( (scr, index) => (
|
||||
<ResponseDisplayScreenshot key={"screenshot" + index + 'fortask' + task.id} task={task} {...scr}
|
||||
displayType={displayType} expand={expand} />
|
||||
))
|
||||
}
|
||||
{browserScriptData?.plaintext !== undefined &&
|
||||
<ResponseDisplayPlaintext plaintext={browserScriptData["plaintext"]} task={task}
|
||||
expand={expand} displayType={displayType} />
|
||||
}
|
||||
{browserScriptData?.table?.map( (table, index) => (
|
||||
|
||||
<ResponseDisplayTable callback_id={task.callback_id} task={task} expand={expand}
|
||||
table={table} key={"tablefortask" + task.id + "table" + index}
|
||||
displayType={displayType}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{browserScriptData?.download?.map( (dl, index) => (
|
||||
<ResponseDisplayDownload download={dl} task={task} displayType={displayType}
|
||||
key={"download" + index + "fortask" + task.id} />
|
||||
))
|
||||
}
|
||||
{browserScriptData?.search?.map( (s, index) => (
|
||||
<ResponseDisplaySearch search={s} task={task} displayType={displayType}
|
||||
key={"searchlink" + index + "fortask" + task.id} />
|
||||
))
|
||||
}
|
||||
{browserScriptData?.media?.map( (s, index) => (
|
||||
<ResponseDisplayMedia key={"searchmedia" + index + "fortask" + task.id}
|
||||
displayType={displayType}
|
||||
task={task} media={s} expand={expand} />
|
||||
))}
|
||||
{browserScriptData?.graph !== undefined &&
|
||||
<ResponseDisplayGraph graph={browserScriptData.graph} task={task}
|
||||
expand={expand} displayType={displayType} />
|
||||
}
|
||||
</React.Fragment>
|
||||
localViewBrowserScript ? (
|
||||
<ResponseDisplayBrowserScriptComponent expand={expand} displayType={displayType}
|
||||
task={task} browserScriptData={browserScriptData}
|
||||
output={output}/>
|
||||
) : (
|
||||
<ResponseDisplayPlaintext plaintext={output} task={task} expand={expand} displayType={displayType}/>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export function ResponseDisplayBrowserScriptComponent({output, browserScriptData, task, expand, displayType}) {
|
||||
return (
|
||||
<>
|
||||
{Object.keys(browserScriptData).length > 0 ? (
|
||||
<>
|
||||
{browserScriptData?.screenshot?.map( (scr, index) => (
|
||||
<ResponseDisplayScreenshot key={"screenshot" + index + 'fortask' + task.id} task={task} {...scr}
|
||||
displayType={displayType} expand={expand} />
|
||||
))
|
||||
}
|
||||
{browserScriptData?.plaintext !== undefined &&
|
||||
<ResponseDisplayPlaintext plaintext={browserScriptData["plaintext"]} task={task}
|
||||
expand={expand} displayType={displayType} />
|
||||
}
|
||||
{browserScriptData?.table?.map( (table, index) => (
|
||||
<ResponseDisplayTable callback_id={task.callback_id} task={task} expand={expand}
|
||||
table={table} key={"tablefortask" + task.id + "table" + index}
|
||||
displayType={displayType}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{browserScriptData?.download?.map( (dl, index) => (
|
||||
<ResponseDisplayDownload download={dl} task={task} displayType={displayType}
|
||||
key={"download" + index + "fortask" + task.id} />
|
||||
))
|
||||
}
|
||||
{browserScriptData?.search?.map( (s, index) => (
|
||||
<ResponseDisplaySearch search={s} task={task} displayType={displayType}
|
||||
key={"searchlink" + index + "fortask" + task.id} />
|
||||
))
|
||||
}
|
||||
{browserScriptData?.media?.map( (s, index) => (
|
||||
<ResponseDisplayMedia key={"searchmedia" + index + "fortask" + task.id}
|
||||
displayType={displayType}
|
||||
task={task} media={s} expand={expand} />
|
||||
))}
|
||||
{browserScriptData?.graph !== undefined &&
|
||||
<ResponseDisplayGraph graph={browserScriptData.graph} task={task}
|
||||
expand={expand} displayType={displayType} />
|
||||
}
|
||||
{browserScriptData.tabs && browserScriptData.tabs.length > 0 &&
|
||||
<ResponseDisplayTabs task={task} expand={expand} displayType={displayType} tabs={browserScriptData.tabs} output={output} />
|
||||
}
|
||||
</>
|
||||
) : (
|
||||
<ResponseDisplayPlaintext plaintext={output} task={task} expand={expand} displayType={displayType}/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import WrapTextIcon from '@mui/icons-material/WrapText';
|
||||
import { useReactiveVar } from '@apollo/client';
|
||||
import { meState } from '../../../cache';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import HeightIcon from '@mui/icons-material/Height';
|
||||
|
||||
const getInteractiveTaskingQuery = gql`
|
||||
${taskingDataFragment}
|
||||
@@ -168,7 +169,6 @@ export const GetOutputFormatAll = ({data, myTask, taskID, useASNIColor, message
|
||||
|
||||
}
|
||||
|
||||
|
||||
const InteractiveMessageTypes = [
|
||||
{"name": "None", "value": -1, "text": "None"},
|
||||
{"name": "Tab", "value": 13, "text": "^I"},
|
||||
@@ -202,8 +202,9 @@ const EnterOptions = [
|
||||
];
|
||||
export const ResponseDisplayInteractive = (props) =>{
|
||||
const me = useReactiveVar(meState);
|
||||
const theme = useTheme();
|
||||
const [backdropOpen, setBackdropOpen] = React.useState(false);
|
||||
const [scrollToBottom, setScrollToBottom] = React.useState(false);
|
||||
const [scrollToBottom, setScrollToBottom] = React.useState(true);
|
||||
const pageSize = React.useRef(100);
|
||||
const highestFetched = React.useRef(0);
|
||||
const [taskData, setTaskData] = React.useState([]);
|
||||
@@ -218,6 +219,7 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
const [useASNIColor, setUseANSIColor] = React.useState(true);
|
||||
const [showTaskStatus, setShowTaskStatus] = React.useState(true);
|
||||
const [wrapText, setWrapText] = React.useState(true);
|
||||
const [autoScroll, setAutoScroll] = React.useState(true);
|
||||
const {loading: loadingTasks} = useSubscription(getInteractiveTaskingQuery, {
|
||||
variables: {parent_task_id: props.task.id},
|
||||
onError: data => {
|
||||
@@ -279,8 +281,7 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
if(backdropOpen){
|
||||
setBackdropOpen(false);
|
||||
}
|
||||
|
||||
}, [highestFetched.current, rawResponses, props.task.id, backdropOpen, taskIDRef.current]);
|
||||
}, [highestFetched.current, rawResponses, props.task.id, backdropOpen, taskIDRef.current, autoScroll]);
|
||||
useSubscription(subResponsesQuery, {
|
||||
variables: {task_id: props.task.id},
|
||||
fetchPolicy: "no-cache",
|
||||
@@ -367,11 +368,14 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
const toggleWrapText = () => {
|
||||
setWrapText(!wrapText);
|
||||
}
|
||||
useEffect( () => {
|
||||
if(scrollToBottom){
|
||||
messagesEndRef.current.scrollIntoView();
|
||||
const toggleAutoScroll = () => {
|
||||
setAutoScroll(!autoScroll);
|
||||
}
|
||||
useEffect(() => {
|
||||
if(autoScroll){
|
||||
messagesEndRef?.current?.scrollIntoView({ behavior: "auto", block: "nearest" });
|
||||
}
|
||||
}, [scrollToBottom]);
|
||||
}, [props.responseRef?.current?.scrollHeight, autoScroll]);
|
||||
React.useEffect( () => {
|
||||
if(loadingTasks){
|
||||
setTaskData([]);
|
||||
@@ -388,8 +392,13 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
|
||||
<div style={{
|
||||
display: "flex", overflowY: "auto",
|
||||
position: "relative", height: props.expand ? "100%" : undefined, maxHeight: props.expand ? "100%" : "500px",
|
||||
flexDirection: "column"
|
||||
position: "relative",
|
||||
height: props.expand ? "100%" : undefined,
|
||||
maxHeight: props.expand ? "100%" : "500px",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
backgroundColor: theme.outputBackgroundColor + (theme.palette.mode === 'dark' ? "B0" : "E0"),
|
||||
color: theme.outputTextColor,
|
||||
}}>
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute",}} invisible={false}>
|
||||
<div style={{
|
||||
@@ -408,19 +417,17 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
{props.searchOutput &&
|
||||
<SearchBar onSubmitSearch={onSubmitSearch}/>
|
||||
}
|
||||
<div style={{overflowY: "auto", width: "100%", marginBottom: "5px",
|
||||
<div style={{overflowY: "auto", width: "100%", marginBottom: "5px", height: props.expand ? "100%": undefined,
|
||||
flexGrow: 1, paddingLeft: "10px", minHeight: "50px"}} ref={props.responseRef}
|
||||
id={`ptytask${props.task.id}`}>
|
||||
|
||||
<GetOutputFormatAll data={alloutput}
|
||||
myTask={props.task.operator.username === (me?.user?.username || "")}
|
||||
taskID={props.task.id}
|
||||
useASNIColor={useASNIColor}
|
||||
messagesEndRef={messagesEndRef}
|
||||
showTaskStatus={showTaskStatus}
|
||||
search={props.searchOutput ? search : undefined}
|
||||
wrapText={wrapText}/>
|
||||
|
||||
<GetOutputFormatAll data={alloutput}
|
||||
myTask={props.task.operator.username === (me?.user?.username || "")}
|
||||
taskID={props.task.id}
|
||||
useASNIColor={useASNIColor}
|
||||
messagesEndRef={messagesEndRef}
|
||||
showTaskStatus={showTaskStatus}
|
||||
search={props.searchOutput ? search : undefined}
|
||||
wrapText={wrapText}/>
|
||||
<div ref={messagesEndRef}/>
|
||||
</div>
|
||||
{!props.task?.is_interactive_task &&
|
||||
@@ -429,6 +436,7 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
useASNIColor={useASNIColor} toggleANSIColor={toggleANSIColor}
|
||||
showTaskStatus={showTaskStatus} toggleShowTaskStatus={toggleShowTaskStatus}
|
||||
wrapText={wrapText} toggleWrapText={toggleWrapText}
|
||||
autoScroll={autoScroll} toggleAutoScroll={toggleAutoScroll}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
@@ -442,7 +450,8 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
}
|
||||
const InteractiveTaskingBar = ({
|
||||
task, taskData, useASNIColor, toggleANSIColor,
|
||||
showTaskStatus, toggleShowTaskStatus, wrapText, toggleWrapText
|
||||
showTaskStatus, toggleShowTaskStatus, wrapText, toggleWrapText,
|
||||
autoScroll, toggleAutoScroll
|
||||
}) => {
|
||||
const [inputText, setInputText] = React.useState("");
|
||||
const theme = useTheme();
|
||||
@@ -459,8 +468,8 @@ const InteractiveTaskingBar = ({
|
||||
});
|
||||
const [taskOptionsIndex, setTaskOptionsIndex] = React.useState(-1);
|
||||
const [taskOptions, setTaskOptions] = React.useState([]);
|
||||
const [selectedEnterOption, setSelectedEnterOption] = React.useState(EnterOptions[1]);
|
||||
const [selectedControlOption, setSelectedControlOption] = React.useState(InteractiveMessageTypes[0]);
|
||||
const [selectedEnterOption, setSelectedEnterOption] = React.useState(1);
|
||||
const [selectedControlOption, setSelectedControlOption] = React.useState(0);
|
||||
React.useEffect( () => {
|
||||
const newTaskOptions = taskData.filter( t => t.display_params.length > 1 && (t.interactive_task_type === 0 || t.interactive_task_type === 8));
|
||||
newTaskOptions.sort( (a,b) => a.id > b.id ? -1 : 1);
|
||||
@@ -510,23 +519,23 @@ const InteractiveTaskingBar = ({
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if(event.shiftKey){
|
||||
setInputText(inputText + selectedEnterOption.value);
|
||||
setInputText(inputText + EnterOptions[selectedEnterOption].value);
|
||||
return;
|
||||
}
|
||||
if(event.metaKey || event.ctrlKey){
|
||||
setInputText(inputText + selectedEnterOption.value);
|
||||
setInputText(inputText + EnterOptions[selectedEnterOption].value);
|
||||
return;
|
||||
}
|
||||
if(selectedControlOption.value > 0){
|
||||
let ctrlSequence = selectedControlOption.text;
|
||||
let enterOption = selectedEnterOption.value;
|
||||
if(InteractiveMessageTypes[selectedControlOption].value > 0){
|
||||
let ctrlSequence = InteractiveMessageTypes[selectedControlOption].text;
|
||||
let enterOption = EnterOptions[selectedEnterOption].value;
|
||||
let originalParams = inputText + ctrlSequence + enterOption;
|
||||
// if we're looking at a tab, never send enter along with it
|
||||
if (selectedControlOption.value === 8){
|
||||
if (InteractiveMessageTypes[selectedControlOption].value === 8){
|
||||
originalParams = inputText + ctrlSequence;
|
||||
enterOption = "";
|
||||
// if we're looking at escape, never send enter along with it
|
||||
} else if(selectedControlOption.value === 4){
|
||||
} else if(InteractiveMessageTypes[selectedControlOption].value === 4){
|
||||
originalParams = ctrlSequence + inputText;
|
||||
enterOption = "";
|
||||
}
|
||||
@@ -539,16 +548,16 @@ const InteractiveTaskingBar = ({
|
||||
parameter_group_name: "default",
|
||||
parent_task_id: task.id,
|
||||
is_interactive_task: true,
|
||||
interactive_task_type: selectedControlOption.value,
|
||||
interactive_task_type: InteractiveMessageTypes[selectedControlOption].value,
|
||||
}})
|
||||
}else {
|
||||
// no control option selected, just send data along as input
|
||||
createTask({variables: {
|
||||
callback_id: task.callback.display_id,
|
||||
command: task.command.cmd,
|
||||
params: inputText + selectedEnterOption.value,
|
||||
params: inputText + EnterOptions[selectedEnterOption].value,
|
||||
tasking_location: "command_line",
|
||||
original_params: inputText + selectedEnterOption.value,
|
||||
original_params: inputText + EnterOptions[selectedEnterOption].value,
|
||||
parameter_group_name: "default",
|
||||
parent_task_id: task.id,
|
||||
is_interactive_task: true,
|
||||
@@ -556,7 +565,7 @@ const InteractiveTaskingBar = ({
|
||||
}})
|
||||
}
|
||||
setInputText("");
|
||||
setSelectedControlOption(InteractiveMessageTypes[0]);
|
||||
setSelectedControlOption(0);
|
||||
setTaskOptionsIndex(-1);
|
||||
}
|
||||
const onChangeSelect = (event) => {
|
||||
@@ -573,20 +582,29 @@ const InteractiveTaskingBar = ({
|
||||
<div style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-end",
|
||||
backgroundColor: theme.palette.graphGroupRGBA,
|
||||
//backgroundColor: theme.outputBackgroundColor + (theme.palette.mode === 'dark' ? "F0" : "50"),
|
||||
color: theme.outputTextColor,
|
||||
paddingTop: "5px",
|
||||
border: "1px solid grey",
|
||||
borderRadius: "4px",
|
||||
width: "100%"}}>
|
||||
<FormControl style={{width: "10rem", marginTop: "2px"}} >
|
||||
<InputLabel id="control-label" style={{}}>Controls</InputLabel>
|
||||
<FormControl style={{width: "10rem", marginTop: "2px", color: theme.outputTextColor}} >
|
||||
<InputLabel id="control-label" style={{color: theme.outputTextColor}}>Controls</InputLabel>
|
||||
<Select
|
||||
labelId="control-label"
|
||||
id="control-select"
|
||||
value={selectedControlOption}
|
||||
onChange={onChangeSelect}
|
||||
input={<Input style={{margin: 0}} />}
|
||||
sx={{
|
||||
// Target the icon element directly within the Select
|
||||
'.MuiSelect-icon': {
|
||||
color: theme.outputTextColor, // Set your desired color
|
||||
},
|
||||
}}
|
||||
input={<Input style={{margin: 0, color: theme.outputTextColor}} />}
|
||||
>
|
||||
{InteractiveMessageTypes.map( (opt) => (
|
||||
<MenuItem value={opt} key={opt.name}>{opt.name}</MenuItem>
|
||||
{InteractiveMessageTypes.map( (opt,index) => (
|
||||
<MenuItem value={index} key={opt.name}>{opt.name}</MenuItem>
|
||||
) )}
|
||||
</Select>
|
||||
</FormControl>
|
||||
@@ -594,29 +612,34 @@ const InteractiveTaskingBar = ({
|
||||
<MythicTextField autoFocus={true} maxRows={5} multiline={true} onChange={onInputChange} onEnter={submitTask}
|
||||
value={inputText} variant={"standard"} placeholder={">_ type here..."} inline={true}
|
||||
debounceDelay={50}
|
||||
marginBottom={"0px"} InputProps={{style: { width: "100%"}}}/>
|
||||
<FormControl style={{width: "7rem"}} >
|
||||
marginBottom={"0px"} InputProps={{style: { width: "100%", color: theme.outputTextColor}}}/>
|
||||
<FormControl style={{width: "7rem", color: theme.outputTextColor}} >
|
||||
<Select
|
||||
labelId="linefeed-label"
|
||||
id="linefeed-control"
|
||||
value={selectedEnterOption}
|
||||
autoWidth
|
||||
sx={{
|
||||
// Target the icon element directly within the Select
|
||||
'.MuiSelect-icon': {
|
||||
color: theme.outputTextColor, // Set your desired color
|
||||
},
|
||||
}}
|
||||
onChange={onChangeSelectEnterOption}
|
||||
input={<Input />}
|
||||
input={<Input style={{color: theme.outputTextColor}} />}
|
||||
IconComponent={KeyboardReturnIcon}
|
||||
>
|
||||
{EnterOptions.map( (opt) => (
|
||||
<MenuItem value={opt} key={opt.name}>{opt.name}</MenuItem>
|
||||
{EnterOptions.map( (opt,index) => (
|
||||
<MenuItem value={index} key={opt.name}>{opt.name}</MenuItem>
|
||||
) )}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<MythicStyledTooltip title={useASNIColor ? "Disable ANSI Color" : "Enable ANSI Color"} >
|
||||
<IconButton onClick={toggleANSIColor} style={{paddingLeft: 0, paddingRight: 0}} disableRipple={true} disableFocusRipple={true}>
|
||||
<IconButton onClick={toggleANSIColor} style={{paddingLeft: 0, paddingRight: 0,}} disableRipple={true} disableFocusRipple={true}>
|
||||
<PaletteIcon color={useASNIColor ? "success" : "secondary"}
|
||||
style={{cursor: "pointer"}}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={showTaskStatus ? "Hide Task Status" : "Show Task Status"} >
|
||||
<IconButton onClick={toggleShowTaskStatus} style={{paddingLeft: 0, paddingRight: 0}} disableRipple={true} disableFocusRipple={true}>
|
||||
@@ -624,7 +647,6 @@ const InteractiveTaskingBar = ({
|
||||
style={{cursor: "pointer",}}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={wrapText ? "Unwrap Text" : "Wrap Text"} >
|
||||
<IconButton onClick={toggleWrapText} style={{paddingLeft: 0, paddingRight: 0}} disableRipple={true} disableFocusRipple={true}>
|
||||
@@ -632,7 +654,13 @@ const InteractiveTaskingBar = ({
|
||||
style={{cursor: "pointer"}}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={autoScroll ? "Stop Auto Scroll" : "Auto Scroll"} >
|
||||
<IconButton onClick={toggleAutoScroll} style={{paddingLeft: 0, paddingRight: 0}} disableRipple={true} disableFocusRipple={true}>
|
||||
<HeightIcon color={autoScroll ? "success" : "secondary"}
|
||||
style={{cursor: "pointer"}}
|
||||
/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -41,6 +41,7 @@ import 'ace-builds/src-noconflict/mode-apache_conf';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import TabContext from '@mui/lab/TabContext';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import {previewFileQuery} from "../Search/FileMetaTable";
|
||||
import { useMutation, gql, useQuery } from '@apollo/client';
|
||||
@@ -48,12 +49,20 @@ import CodeIcon from '@mui/icons-material/Code';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import {b64DecodeUnicode} from "./ResponseDisplay";
|
||||
import {MythicDialog, TableRowSizeCell} from "../../MythicComponents/MythicDialog";
|
||||
import {Table, TableHead, TableRow, TableBody} from '@mui/material';
|
||||
import {Table, TableHead, TableRow, TableBody, TableCell, Paper} from '@mui/material';
|
||||
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
|
||||
import WarningOutlinedIcon from '@mui/icons-material/WarningOutlined';
|
||||
import {TagsDisplay, ViewEditTags} from "../../MythicComponents/MythicTag";
|
||||
import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery";
|
||||
import {ResponseDisplayScreenshotModal} from "./ResponseDisplayScreenshotModal";
|
||||
import initSQLJS from 'sql.js';
|
||||
import TabPanel from '@mui/lab/TabPanel';
|
||||
import TabList from '@mui/lab/TabList';
|
||||
import {Backdrop, CircularProgress} from '@mui/material';
|
||||
// Required to let webpack 4 know it needs to copy the wasm file to our assets
|
||||
// eslint-disable-next-line import/no-webpack-loader-syntax
|
||||
import sqlWasm from "!!file-loader?name=sql-wasm-[contenthash].wasm!sql.js/dist/sql-wasm.wasm";
|
||||
import {copyStringToClipboard} from "../../utilities/Clipboard";
|
||||
|
||||
export const modeOptions = ["csharp", "golang", "html", "json", "markdown", "ruby", "python", "java",
|
||||
"javascript", "yaml", "toml", "swift", "sql", "rust", "powershell", "pgsql", "perl", "php", "objectivec",
|
||||
@@ -121,7 +130,6 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
<div style={{display: "flex", height: "100%", flexDirection: "column"}}>
|
||||
<DisplayFileMetaData fileMetaData={fileMetaData} />
|
||||
<AppBar color={'default'} position='static' className={"no-box-shadow"}>
|
||||
|
||||
<Tabs
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
@@ -139,6 +147,7 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
<Tab className={value === 0 ? "selectedCallback": ""} label={"Preview"}></Tab>
|
||||
<Tab className={value === 1 ? "selectedCallback": ""} label={"Text"}></Tab>
|
||||
<Tab className={value === 2 ? "selectedCallback": ""} label={"Hex"}></Tab>
|
||||
<Tab className={value === 3 ? "selectedCallback": ""} label={"Database"}></Tab>
|
||||
<MythicStyledTooltip title={"Download the file"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<Button style={{}} size={"small"} href={"/direct/download/" + media.agent_file_id}
|
||||
download color={"success"}>
|
||||
@@ -147,7 +156,7 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
</MythicStyledTooltip>
|
||||
</Tabs>
|
||||
</AppBar>
|
||||
<div hidden={value !== 0} style={{height: "100%"}} role='tabpanel' >
|
||||
<div hidden={value !== 0} style={{height: "100%", overflow: "auto"}} role='tabpanel' >
|
||||
{value === 0 &&
|
||||
<DisplayMedia agent_file_id={media?.agent_file_id || ""}
|
||||
task={task} filename={media?.filename || undefined}
|
||||
@@ -155,7 +164,7 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
expand={expand} />
|
||||
}
|
||||
</div>
|
||||
<div hidden={value !== 1} style={{height: "100%"}} role='tabpanel' >
|
||||
<div hidden={value !== 1} style={{height: "100%", overflow: "auto"}} role='tabpanel' >
|
||||
{value === 1 &&
|
||||
<DisplayText agent_file_id={media?.agent_file_id || ""}
|
||||
task={task} filename={media?.filename || undefined}
|
||||
@@ -163,7 +172,7 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
expand={expand} preview />
|
||||
}
|
||||
</div>
|
||||
<div hidden={value !== 2} style={{height: "100%"}} role='tabpanel' >
|
||||
<div hidden={value !== 2} style={{height: "100%", overflow: "auto"}} role='tabpanel' >
|
||||
{value === 2 &&
|
||||
<DisplayHex agent_file_id={media?.agent_file_id || ""}
|
||||
task={task} filename={media?.filename || undefined}
|
||||
@@ -172,6 +181,15 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
}
|
||||
|
||||
</div>
|
||||
<div hidden={value !== 3} style={{height: "100%", overflow: "auto"}} role='tabpanel' >
|
||||
{value === 3 &&
|
||||
<DisplayDatabase agent_file_id={media?.agent_file_id || ""}
|
||||
task={task} filename={media?.filename || undefined}
|
||||
fileMetaData={fileMetaData}
|
||||
expand={expand} />
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -353,7 +371,7 @@ export const DisplayMedia = ({agent_file_id, filename, expand, task, fileMetaDat
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const MaxRenderSize = 2000000;
|
||||
const MaxRenderSize = 2000000; // 2MB
|
||||
const DisplayFileMetaData = ({fileMetaData}) => {
|
||||
return (
|
||||
<Table style={{marginLeft: "0px", width: "100%", tableLayout: "fixed"}}>
|
||||
@@ -418,7 +436,10 @@ const DisplayText = ({agent_file_id, expand, filename, preview, fileMetaData}) =
|
||||
setLimitedPreviewWarning(true);
|
||||
}
|
||||
}else{
|
||||
snackActions.error(data.previewFile.error)
|
||||
console.log(data.previewFile.error);
|
||||
if(data.previewFile.error !== "EOF"){
|
||||
snackActions.error(data.previewFile.error);
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (data) => {
|
||||
@@ -478,7 +499,7 @@ const DisplayText = ({agent_file_id, expand, filename, preview, fileMetaData}) =
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [agent_file_id, filename]);
|
||||
}, [agent_file_id, filename, fileMetaData]);
|
||||
const onChangeMode = (event) => {
|
||||
setMode(event.target.value);
|
||||
}
|
||||
@@ -536,7 +557,7 @@ const DisplayText = ({agent_file_id, expand, filename, preview, fileMetaData}) =
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
<div style={{display: "flex", flexGrow: 1, height: "100%", minHeight: "375px"}}>
|
||||
<div style={{display: "flex", flexGrow: 1, height: "100%"}}>
|
||||
<AceEditor
|
||||
ref={currentContentRef}
|
||||
mode={mode}
|
||||
@@ -634,4 +655,255 @@ const DisplayHex = ({agent_file_id, expand, fileMetaData}) => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const MAX_ROWS = 1000;
|
||||
const DisplayDatabase = ({agent_file_id, expand, fileMetaData}) => {
|
||||
const theme = useTheme();
|
||||
const queryCountRef = React.useRef(1);
|
||||
const currentContentRef = React.useRef();
|
||||
const [selectedTab, setSelectedTab] = React.useState(0);
|
||||
const [sql, setSql] = React.useState(null);
|
||||
const [query, setQuery] = React.useState("select * from cookies;");
|
||||
const [results, setResults] = React.useState([]);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const onChangeText = (data) => {
|
||||
setQuery(data);
|
||||
}
|
||||
const removeQuery = (event, index) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if(selectedTab === index){
|
||||
if(index !== 0){
|
||||
setSelectedTab(index-1);
|
||||
}
|
||||
}
|
||||
setResults(results.toSpliced(index, 1));
|
||||
}
|
||||
const onSubmitQuery = (event) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
if(sql){
|
||||
try{
|
||||
const result = sql.exec(query);
|
||||
//console.log(result);
|
||||
if(result.length > 0){
|
||||
setResults([...results, {name: `Query: ${queryCountRef.current}`, query: query, results: result[0]}]);
|
||||
setSelectedTab(queryCountRef.current - 1);
|
||||
queryCountRef.current += 1;
|
||||
}
|
||||
}catch(e){
|
||||
console.log(e);
|
||||
snackActions.error(e.message);
|
||||
}
|
||||
} else {
|
||||
console.log(sql);
|
||||
snackActions.error("Failed to load as SQL Database");
|
||||
}
|
||||
}
|
||||
const onChangeTab = (_, v) => {
|
||||
const tabQuery = results[v]?.query;
|
||||
setQuery(tabQuery);
|
||||
setSelectedTab(v);
|
||||
}
|
||||
const onCopyToClipboard = (data) => {
|
||||
let result = copyStringToClipboard(data);
|
||||
if(result){
|
||||
snackActions.success("Copied text!");
|
||||
}else{
|
||||
snackActions.error("Failed to copy text");
|
||||
}
|
||||
}
|
||||
const onSaveOutputCSV = (result) => {
|
||||
const filteredData = result.results.values;
|
||||
let outputHeaders = "";
|
||||
let outputRow = "";
|
||||
outputHeaders = result.results.columns.join(",");
|
||||
for(let i = 0; i < filteredData.length; i++){
|
||||
outputRow += filteredData[i].join(",") + "\n"
|
||||
}
|
||||
onCopyToClipboard(outputHeaders + "\n" + outputRow);
|
||||
}
|
||||
const onSaveOutputPrettyPrint = (result) => {
|
||||
let tableData = result.results.values;
|
||||
if(tableData.length === 0){return}
|
||||
let outputHeaders = result.results.columns;
|
||||
let outputHeadersMaxLength = outputHeaders.reduce( (prev, cur) => {
|
||||
return [...prev, cur.length]
|
||||
}, []);
|
||||
tableData.map( cur => {
|
||||
for(let i = 0; i < outputHeaders.length; i++){
|
||||
if(outputHeadersMaxLength[i] < String(cur[i]).length){
|
||||
outputHeadersMaxLength[i] = String(cur[i]).length;
|
||||
}
|
||||
}
|
||||
});
|
||||
let allOutput = "";
|
||||
for(let i = 0; i < outputHeaders.length; i++){
|
||||
let remainingSpace = Math.max(0, outputHeadersMaxLength[i] - outputHeaders[i].length);
|
||||
allOutput += " " + outputHeaders[i].toUpperCase() + " ".repeat(remainingSpace + 2);
|
||||
}
|
||||
allOutput += "\n";
|
||||
for(let i = 0; i < outputHeaders.length; i++){
|
||||
let remainingSpace = Math.max(0, outputHeadersMaxLength[i] - outputHeaders[i].length);
|
||||
allOutput += " " + "-".repeat(outputHeaders[i].length) + " ".repeat(remainingSpace + 2);
|
||||
}
|
||||
allOutput += "\n";
|
||||
for(let i = 0; i < tableData.length; i++){
|
||||
for(let j = 0; j < outputHeaders.length; j++){
|
||||
let remainingSpace = Math.max(0, outputHeadersMaxLength[j] - String(tableData[i][j]).length);
|
||||
allOutput += "| " + String(tableData[i][j]) + " ".repeat(remainingSpace) + " ";
|
||||
}
|
||||
allOutput += " |\n"
|
||||
}
|
||||
onCopyToClipboard(allOutput);
|
||||
}
|
||||
React.useEffect( () => {
|
||||
async function initialize(){
|
||||
const newSQL = await initSQLJS({locateFile: () => sqlWasm});
|
||||
fetch('/direct/view/' + agent_file_id).then((response) => {
|
||||
if(response.status !== 200){
|
||||
setLoading(false);
|
||||
snackActions.warning("Failed to fetch contents from Mythic");
|
||||
return;
|
||||
}
|
||||
response.arrayBuffer().then( data => {
|
||||
const SQL = new newSQL.Database(new Uint8Array(data));
|
||||
if(SQL){
|
||||
setSql(SQL);
|
||||
}
|
||||
setLoading(false);
|
||||
}).catch(error => {
|
||||
setLoading(false);
|
||||
snackActions.warning("Error getting contents 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());
|
||||
}
|
||||
setLoading(false);
|
||||
console.log("There was an error!", error);
|
||||
});
|
||||
}
|
||||
initialize();
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
// Execute query on Ctrl+Enter or Cmd+Enter when in the SQL editor
|
||||
if (e.shiftKey && e.key === 'Enter') {
|
||||
onSubmitQuery(e);
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [onSubmitQuery]);
|
||||
return (
|
||||
<div style={{display: "flex", height: "100%", flexDirection: "column", width: "100%", position: "relative"}}>
|
||||
<Backdrop open={loading} onClick={()=>{setLoading(false);}} style={{zIndex: 2, position: "absolute"}}>
|
||||
<CircularProgress color="inherit" disableShrink />
|
||||
</Backdrop>
|
||||
<div style={{display: "inline-flex", flexDirection: "row", alignItems: "center", borderBottom: "1px solid grey"}}>
|
||||
<AceEditor
|
||||
className={"roundedBottomCorners"}
|
||||
ref={currentContentRef}
|
||||
mode={'sql'}
|
||||
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
|
||||
fontSize={14}
|
||||
showGutter={ true}
|
||||
onChange={onChangeText}
|
||||
highlightActiveLine={false}
|
||||
showPrintMargin={false}
|
||||
value={query}
|
||||
height={"100%"}
|
||||
maxLines={3}
|
||||
width={"100%"}
|
||||
wrapEnabled={true}
|
||||
minLines={2}
|
||||
setOptions={{
|
||||
showLineNumbers: true,
|
||||
tabSize: 4,
|
||||
useWorker: false,
|
||||
showInvisibles: false,
|
||||
}}/>
|
||||
<Button color={"success"} variant={"contained"} onClick={onSubmitQuery} >Query</Button>
|
||||
</div>
|
||||
<TabContext style={{width: "100%", height: "100%", borderBottom: "1px solid grey", overflow: "auto"}} value={selectedTab}>
|
||||
<TabList onChange={onChangeTab} indicatorColor='secondary'
|
||||
textColor='primary'
|
||||
sx={{
|
||||
'& .Mui-selected': {
|
||||
color: "unset !important"
|
||||
}
|
||||
}}>
|
||||
{results.map((result, index) => (
|
||||
<Tab key={result.query + index} style={{padding: 0, paddingLeft: "10px", paddingRight: "10px"}} label={
|
||||
<div style={{display: "inline-block"}}>
|
||||
{result.name}
|
||||
<IconButton style={{padding: 0, marginLeft: "5px", marginRight: "5px", marginBottom: "5px"}}
|
||||
color={"error"}
|
||||
onClick={(e) => removeQuery(e, index)}>
|
||||
x
|
||||
</IconButton>
|
||||
</div>} value={index} className={selectedTab === index ? "selectedCallback" : ""} />
|
||||
))}
|
||||
</TabList>
|
||||
|
||||
{results.map((result, index) => (
|
||||
<div key={index} style={{display: selectedTab === index ? "flex" : "none",
|
||||
width: "100%", height: "100%", overflow: "auto",
|
||||
flexDirection: "column"}}>
|
||||
{result?.results?.values?.length >= MAX_ROWS &&
|
||||
<Paper key={"overflow" + index} style={{display: selectedTab === index ? "flex" : "none",
|
||||
justifyContent: "center", hidden: selectedTab !== index}}>
|
||||
<WarningOutlinedIcon color={"warning"}></WarningOutlinedIcon>
|
||||
{"Output is truncated to first " + MAX_ROWS + " rows"}
|
||||
<WarningOutlinedIcon color={"warning"}></WarningOutlinedIcon>
|
||||
</Paper>
|
||||
}
|
||||
<div key={"output" + index} style={{display: selectedTab === index ? "flex" : "none", alignItems: "center", width: "100%"}}>
|
||||
{"Save Output As: "}
|
||||
<Button variant={"outlined"} style={{marginLeft: "10px"}} onClick={() => onSaveOutputCSV(result)} >CSV</Button>
|
||||
<Button variant={"outlined"} style={{marginLeft: "10px"}} onClick={() => onSaveOutputPrettyPrint(result)} >Pretty Print</Button>
|
||||
</div>
|
||||
<TabPanel value={index} key={"tabpanel" + index}
|
||||
style={{padding: 0, height: "100%", width: "100%", overflow: "auto", position: "relative",
|
||||
display: selectedTab === index ? "flex" : "none",
|
||||
flexDirection: "column"}} >
|
||||
<DisplayDatabaseResult result={result?.results} />
|
||||
</TabPanel>
|
||||
</div>
|
||||
))}
|
||||
</TabContext>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const DisplayDatabaseResult = ({result}) => {
|
||||
const [columns, setColumns] = React.useState(result.columns);
|
||||
const [rows, setRows] = React.useState(result.values?.slice(0, MAX_ROWS));
|
||||
return (
|
||||
<Table stickyHeader style={{width: "100%", height: "100%", position: "absolute"}}>
|
||||
<TableHead >
|
||||
<TableRow>
|
||||
{columns.map((column, index) => (
|
||||
<TableCell key={"header" + index}>{column}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row, index1) => (
|
||||
<TableRow key={"row" + index1} hover>
|
||||
{columns.map((_, index2) => (
|
||||
<TableCell key={"row" + index1+ "cell" + index2}>
|
||||
{row[index2]}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import WrapTextIcon from '@mui/icons-material/WrapText';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import { IconButton } from '@mui/material';
|
||||
import { IconButton, Select } from '@mui/material';
|
||||
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
|
||||
import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
|
||||
import { useReactiveVar } from '@apollo/client';
|
||||
@@ -16,6 +16,7 @@ import { meState } from '../../../cache';
|
||||
import CodeIcon from '@mui/icons-material/Code';
|
||||
import {GetOutputFormatAll} from "./ResponseDisplayInteractive";
|
||||
import PaletteIcon from '@mui/icons-material/Palette';
|
||||
import Input from '@mui/material/Input';
|
||||
|
||||
const MaxRenderSize = 2000000;
|
||||
export const ResponseDisplayPlaintext = (props) =>{
|
||||
@@ -96,14 +97,25 @@ export const ResponseDisplayPlaintext = (props) =>{
|
||||
return (
|
||||
<div style={{display: "flex", height: "100%", flexDirection: "column"}}>
|
||||
{showOptions &&
|
||||
<div style={{display: "inline-flex", flexDirection: "row", alignItems: "center"}}>
|
||||
<FormControl sx={{ display: "inline-block", marginLeft: "10px" }} size="small">
|
||||
<div style={{display: "inline-flex", flexDirection: "row", alignItems: "center",
|
||||
color: theme.outputTextColor,
|
||||
backgroundColor: theme.outputBackgroundColor + (theme.palette.mode === 'dark' ? "D0" : "20"),}}>
|
||||
<FormControl sx={{ display: "inline-block", marginLeft: "10px", color: theme.outputTextColor, }} size="small">
|
||||
<TextField
|
||||
label={"Syntax"}
|
||||
select
|
||||
margin={"dense"}
|
||||
size={"small"}
|
||||
style={{display: "inline-block", width: "100%"}}
|
||||
sx={{
|
||||
// Target the icon element directly within the Select
|
||||
'.MuiSelect-icon': {
|
||||
color: theme.outputTextColor, // Set your desired color
|
||||
},
|
||||
'.MuiInputBase-root': {
|
||||
color: theme.outputTextColor,
|
||||
}
|
||||
}}
|
||||
style={{display: "inline-block", width: "100%",}}
|
||||
value={mode}
|
||||
onChange={onChangeMode}
|
||||
>
|
||||
@@ -139,7 +151,6 @@ export const ResponseDisplayPlaintext = (props) =>{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
zIndex: 1,
|
||||
backgroundColor: theme.palette.secondary.main
|
||||
}}>
|
||||
{showOptions &&
|
||||
<UnfoldLessIcon onClick={onChangeShowOptions}
|
||||
@@ -189,7 +200,6 @@ export const ResponseDisplayPlaintext = (props) =>{
|
||||
//autoScrollEditorIntoView={true}
|
||||
wrapEnabled={props.displayType !== 'console' ? wrapText : true}
|
||||
minLines={1}
|
||||
//maxLines={props.expand ? 50 : 20}
|
||||
setOptions={{
|
||||
showLineNumbers: props.displayType !== 'console',
|
||||
tabSize: 4,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, {useEffect, useRef} from 'react';
|
||||
import {Button} from '@mui/material';
|
||||
import { MythicViewJSONAsTableDialog, MythicDialog } from '../../MythicComponents/MythicDialog';
|
||||
import {MythicViewJSONAsTableDialog, MythicDialog, MythicModifyStringDialog} from '../../MythicComponents/MythicDialog';
|
||||
import { MythicDisplayTextDialog } from '../../MythicComponents/MythicDisplayTextDialog';
|
||||
import { ResponseDisplayTableDialogTable } from './ResponseDisplayTableDialogTable';
|
||||
import Paper from '@mui/material/Paper';
|
||||
@@ -89,7 +89,7 @@ export const getIconName = (iconName) => {
|
||||
case "camera":
|
||||
return faCamera;
|
||||
default:
|
||||
return faFileAlt;
|
||||
return iconName;
|
||||
}
|
||||
}
|
||||
export const getIconColor = (theme, color) => {
|
||||
@@ -360,9 +360,9 @@ const ResponseDisplayTableActionCellButton = ({cellData, callback_id}) => {
|
||||
<React.Fragment>
|
||||
<MythicStyledTooltip title={cellData?.button?.hoverText || "Submit Task"}>
|
||||
<Button size="small" onClick={() => setOpenTaskingButton(true)} disabled={cellData?.button?.disabled || false} color="warning"
|
||||
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.disabled ? "unset" : getIconColor(theme, cellData?.button?.startIconColor || "")}}/> : null}
|
||||
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.disabled ? "unset" : getIconColor(theme, cellData?.button?.startIconColor || "")}}/> : undefined}
|
||||
style={{...actionCellButtonStyle}}
|
||||
>{cellData?.button?.name ? cellData?.button?.name : cellData?.button?.startIcon ? null : "Submit Task"}</Button>
|
||||
>{cellData?.button?.name ? cellData?.button?.name : cellData?.button?.startIcon ? undefined : "Submit Task"}</Button>
|
||||
</MythicStyledTooltip>
|
||||
{openTaskingButton &&
|
||||
<TaskFromUIButton ui_feature={cellData?.button?.ui_feature || " "}
|
||||
@@ -408,10 +408,10 @@ const ResponseDisplayTableActionCellButton = ({cellData, callback_id}) => {
|
||||
table={taskingData?.value || {}} callback_id={callback_id} onClose={finishedViewingData} />}
|
||||
/>
|
||||
}
|
||||
<Button size="small" variant={"contained"} ref={dropdownAnchorRef}
|
||||
<Button size="small" ref={dropdownAnchorRef}
|
||||
onClick={() => setOpenDropdownButton(true)} disabled={cellData?.button?.disabled || false}
|
||||
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.disabled ? "unset" : getIconColor(theme, cellData?.button?.startIconColor || "")}}/> : null}
|
||||
style={{...actionCellButtonStyle}}
|
||||
style={{...actionCellButtonStyle, color: theme.palette.text.primary}}
|
||||
>{cellData?.button?.name || " "} <ArrowDropDownIcon />
|
||||
</Button>
|
||||
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
|
||||
@@ -570,9 +570,9 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
}
|
||||
if (sortData.sortKey === column.plaintext) {
|
||||
if (sortData.sortDirection === 'ASC') {
|
||||
setSortData({sortKey: column.plaintext, sortType: column.type, sortDirection: "DESC"});
|
||||
setSortData({...sortData, sortDirection: "DESC"});
|
||||
} else {
|
||||
setSortData({sortKey: column.plaintext, sortType: column.type, sortDirection: "ASC"});
|
||||
setSortData({sortKey: null, sortType: null, sortDirection: "ASC"});
|
||||
}
|
||||
} else {
|
||||
setSortData({sortKey: column.plaintext, sortType: column.type, sortDirection: "ASC"});
|
||||
@@ -585,9 +585,9 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
});
|
||||
}, [sortedData, table?.headers, callback_id]
|
||||
);
|
||||
const onSubmitFilterOptions = (newFilterOptions) => {
|
||||
setFilterOptions(newFilterOptions);
|
||||
}
|
||||
const onSubmitFilterOptions = (value) => {
|
||||
setFilterOptions({...filterOptions, [selectedColumn.key]: value });
|
||||
}
|
||||
|
||||
const filterOutButtonsFromRowData = (data) => {
|
||||
let rowData = {};
|
||||
@@ -666,7 +666,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
];
|
||||
const contextMenuOptions = [
|
||||
{
|
||||
name: 'Filter Column',
|
||||
name: 'Filter Column', type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -682,7 +682,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Copy Table as JSON",
|
||||
name: "Copy Table as JSON", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -691,7 +691,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Copy Table as CSV",
|
||||
name: "Copy Table as CSV", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -722,7 +722,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Copy Table as TSV",
|
||||
name: "Copy Table as TSV", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -753,7 +753,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Copy Table as PrettyPrint Text",
|
||||
name: "Copy Table as PrettyPrint Text", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -801,7 +801,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
const tableStyle = React.useMemo( () => {
|
||||
return expand ? {flexGrow: 1,
|
||||
minHeight: gridData.length > 0 ? Math.min(maxHeight, dataHeight) : headerHeight,
|
||||
width: "99%", position: "relative"} :
|
||||
width: "100%", position: "relative"} :
|
||||
{height: dataHeight, position: "relative"}
|
||||
}, [expand, dataHeight, gridData]);
|
||||
return (
|
||||
@@ -831,11 +831,16 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={<TableFilterDialog
|
||||
selectedColumn={selectedColumn}
|
||||
filterOptions={filterOptions}
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
onClose={()=>{setOpenContextMenu(false);}} />}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={filterOptions[selectedColumn.key]}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import React from 'react';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import {a11yProps} from "../../MythicComponents/MythicTabPanel";
|
||||
import {ResponseDisplayBrowserScriptComponent} from "./ResponseDisplay";
|
||||
|
||||
function ResponseDisplayTabsLabel(props) {
|
||||
const { label, index, ...other } =
|
||||
props;
|
||||
return (
|
||||
<Tab
|
||||
label={
|
||||
<span>
|
||||
{label}
|
||||
</span>
|
||||
}
|
||||
sx={{
|
||||
'&.Mui-selected': {
|
||||
border: "2px solid grey",
|
||||
borderRadius: "4px",
|
||||
//backgroundColor: 'info.main', // Color when selected
|
||||
},
|
||||
}}
|
||||
wrapped={true}
|
||||
{...a11yProps(index)}
|
||||
{...other}
|
||||
/>
|
||||
);
|
||||
}
|
||||
function ResponseDisplayTabsPanel(props) {
|
||||
const { children, value, index, ...other } =
|
||||
props;
|
||||
const style =
|
||||
props.style === undefined
|
||||
? {
|
||||
display: value === index ? 'flex' : 'none',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
overflowY: "auto",
|
||||
}
|
||||
: props.style;
|
||||
return (
|
||||
<div
|
||||
role='tabpanel'
|
||||
hidden={value !== index}
|
||||
id={`scrollable-auto-tabpanel-${index}`}
|
||||
aria-labelledby={`scrollable-auto-tab-${index}`}
|
||||
style={style}
|
||||
{...other}>
|
||||
{<React.Fragment>{children}</React.Fragment>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export function ResponseDisplayTabs({ tabs, task, expand, displayType, output }) {
|
||||
const mountedRef = React.useRef(true);
|
||||
const [value, setValue] = React.useState(0);
|
||||
const handleChange = (event, newValue) => {
|
||||
setValue(newValue);
|
||||
};
|
||||
React.useEffect( () => {
|
||||
return() => {
|
||||
mountedRef.current = false;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div style={{width: "100%", maxWidth: "100%", display: 'flex', flexDirection: 'column', flexGrow: 1, height: expand ? "100%" : "400px", }}>
|
||||
<AppBar color='default' position={"static"} style={{maxHeight: "25%", overflowY: 'auto', maxWidth: "100%", flexWrap: "no-wrap"}} >
|
||||
<Tabs
|
||||
value={value}
|
||||
variant={"standard"}
|
||||
onChange={handleChange}
|
||||
indicatorColor='primary'
|
||||
textColor='primary'
|
||||
style={{ }}
|
||||
TabIndicatorProps={{style: {
|
||||
display: "none",
|
||||
}}}
|
||||
aria-label='scrollable tabs'>
|
||||
{tabs.map((tab, index) => (
|
||||
<ResponseDisplayTabsLabel
|
||||
key={'tablabel' + task.id + index}
|
||||
index={index}
|
||||
label={tab.title}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</AppBar>
|
||||
{tabs.map((tab, index) => (
|
||||
<ResponseDisplayTabsPanel
|
||||
key={'tabpanel' + task.id + index}
|
||||
value={value}
|
||||
index={index}>
|
||||
<ResponseDisplayBrowserScriptComponent
|
||||
task={task} expand={expand} displayType={displayType} output={output}
|
||||
browserScriptData={tab.content}
|
||||
/>
|
||||
</ResponseDisplayTabsPanel>
|
||||
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {operatorSettingDefaults} from "../../../cache";
|
||||
import {TaskFromUIButton} from "./TaskFromUIButton";
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {faSkullCrossbones} from '@fortawesome/free-solid-svg-icons';
|
||||
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
||||
// Icons for console-view display
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import PersonOutlineIcon from '@mui/icons-material/PersonOutline';
|
||||
@@ -139,19 +140,21 @@ export const StyledAccordionSummary = styled(AccordionSummary)((
|
||||
) => ({
|
||||
[`&.${accordionClasses.root}`]: {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
paddingLeft: 0,
|
||||
//paddingRight: 0,
|
||||
height: "auto",
|
||||
width: "100%",
|
||||
whiteSpace: "break-all",
|
||||
wordBreak: "break-all",
|
||||
userSelect: "text",
|
||||
boxShadow: "unset",
|
||||
backgroundColor: "unset",
|
||||
justifyContent: "flex-start",
|
||||
},
|
||||
[`& .${accordionClasses.content}`]: {
|
||||
margin: 0,
|
||||
height: "100%",
|
||||
padding: 0,
|
||||
width: "inherit",
|
||||
},
|
||||
[`& .${accordionClasses.expandIcon}`]: {
|
||||
margin: 0,
|
||||
@@ -224,7 +227,7 @@ const ColoredTaskDisplay = ({task, theme, children, expanded}) => {
|
||||
setThemeColor(theme.palette.warning.main);
|
||||
}else if(task.status.toLowerCase().includes("processing")){
|
||||
setThemeColor(theme.palette.warning.main);
|
||||
}else if(task.status === "completed" || (task.status === "success" && task.completed)){
|
||||
}else if(task.status === "completed" || task.completed){
|
||||
setThemeColor(theme.palette.success.main);
|
||||
}else{
|
||||
setThemeColor(theme.palette.info.main);
|
||||
@@ -232,7 +235,7 @@ const ColoredTaskDisplay = ({task, theme, children, expanded}) => {
|
||||
}, [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%",
|
||||
height: "100%", borderLeft: "6px solid " + themeColor, paddingLeft: "5px", width: "100%", maxWidth: "100%",
|
||||
borderTopLeftRadius: "4px", borderBottomLeftRadius: expanded ? 0 : "4px"}}>
|
||||
{children}
|
||||
</span>
|
||||
@@ -242,7 +245,7 @@ const GetOperatorDisplay = ({initialHideUsernameValue, task}) => {
|
||||
if(initialHideUsernameValue){
|
||||
return '';
|
||||
}
|
||||
return task.operator.username;
|
||||
return " / " + task.operator.username;
|
||||
}
|
||||
export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayChildren, toggleDisplayChildren, expanded }) => {
|
||||
const [displayComment, setDisplayComment] = React.useState(false);
|
||||
@@ -253,6 +256,7 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh
|
||||
const initialShowHostnameValue = GetMythicSetting({setting_name: "showHostname", default_value: operatorSettingDefaults.showHostname});
|
||||
const initialShowCallbackGroupsValue = GetMythicSetting({setting_name: "showCallbackGroups", default_value: operatorSettingDefaults.showCallbackGroups});
|
||||
const initialTaskTimestampDisplayField = GetMythicSetting({setting_name: "taskTimestampDisplayField", default_value: operatorSettingDefaults.taskTimestampDisplayField});
|
||||
const initialShowOPSECBypassUsername = GetMythicSetting({setting_name: "showOPSECBypassUsername", default_value: operatorSettingDefaults.showOPSECBypassUsername});
|
||||
const displayTimestamp = task[initialTaskTimestampDisplayField] ? task[initialTaskTimestampDisplayField] : task.timestamp;
|
||||
const [openKillTaskButton, setOpenKillTaskButton] = React.useState({open: false});
|
||||
const toggleDisplayComment = (evt) => {
|
||||
@@ -267,7 +271,6 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh
|
||||
if(onClick){
|
||||
onClick(e);
|
||||
}
|
||||
//preventPropagation(e);
|
||||
}
|
||||
const onClickKillIcon = (e, open) => {
|
||||
if(e){
|
||||
@@ -276,7 +279,7 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh
|
||||
setOpenKillTaskButton({open: open});
|
||||
}
|
||||
return (
|
||||
<ColoredTaskDisplay task={task} theme={theme} expanded={expanded} >
|
||||
<ColoredTaskDisplay task={task} theme={theme} expanded={expanded} >
|
||||
<div id={taskDivID} style={{width: "100%"}}>
|
||||
{displayComment && (
|
||||
<React.Fragment>
|
||||
@@ -284,80 +287,92 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh
|
||||
<Typography className={classes.heading} onClick={preventPropagation}>{task.comment}</Typography>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<div style={{lineHeight: 0}}>
|
||||
<div style={{lineHeight: 0}} onClick={onLocalClick}>
|
||||
<Typography className={classes.taskAndTimeDisplay} onClick={preventPropagation}>
|
||||
[{toLocalTime(displayTimestamp, me?.user?.view_utc_time || false)}]
|
||||
{" / "}
|
||||
<span style={{}}>
|
||||
{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>
|
||||
{"/ "}
|
||||
</>
|
||||
}
|
||||
{task?.eventstepinstance !== null &&
|
||||
<>
|
||||
<MythicStyledTooltip title={"Task created via Eventing, click to view entire event flow in separate page"} >
|
||||
<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}
|
||||
{task.has_intercepted_response &&
|
||||
<span style={{display:"inline-flex", whiteSpace: "pre-wrap"}}>
|
||||
<MythicStyledTooltip
|
||||
title={"This task has responses that have been intercepted and changed due to a workflow container"}>
|
||||
<IconButton color={"inherit"} style={{padding: 0}} disableFocusRipple={true}
|
||||
disableRipple={true}>
|
||||
<PlayCircleFilledTwoToneIcon />
|
||||
<CropRotateTwoToneIcon style={{height: "15px", cursor: "default"}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
|
||||
{"/ "}
|
||||
</>
|
||||
}
|
||||
<MythicStyledTooltip title={"View Task in separate page"} >
|
||||
<Link style={{wordBreak: "break-all", color: theme.taskPromptTextColor,}} underline={"always"} target={"_blank"}
|
||||
href={"/new/task/" + task.display_id}>T-{task.display_id}</Link>
|
||||
</MythicStyledTooltip>
|
||||
{" / "}
|
||||
</span>
|
||||
}
|
||||
{task?.eventstepinstance !== null &&
|
||||
<span style={{display:"inline-flex", whiteSpace: "pre-wrap"}}>
|
||||
<MythicStyledTooltip title={"Task created via Eventing, click to view entire event flow in separate page"} >
|
||||
<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 fontSize={"small"} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
|
||||
{" / "}
|
||||
</span>
|
||||
}
|
||||
<span style={{display:"inline-flex", whiteSpace: "pre-wrap"}}>
|
||||
<MythicStyledTooltip title={"View Task in separate page"} >
|
||||
<Link style={{wordBreak: "break-all", color: theme.taskPromptTextColor,}} underline={"always"} target={"_blank"}
|
||||
href={"/new/task/" + task.display_id}>T-{task.display_id}</Link>
|
||||
</MythicStyledTooltip>
|
||||
{!task.completed && task.status_timestamp_processing &&
|
||||
<>
|
||||
<MythicStyledTooltip title={"Task the agent to kill this task"} >
|
||||
<FontAwesomeIcon size={"sm"} icon={faSkullCrossbones} onClick={(e) => onClickKillIcon(e, true)}
|
||||
style={{cursor: "pointer", height: "15px", marginLeft: "5px"}} />
|
||||
</MythicStyledTooltip>
|
||||
</>
|
||||
|
||||
<MythicStyledTooltip title={"Task the agent to kill this task"} >
|
||||
<FontAwesomeIcon size={"sm"} icon={faSkullCrossbones} onClick={(e) => onClickKillIcon(e, true)}
|
||||
style={{cursor: "pointer", height: "12px", marginLeft: "5px"}} />
|
||||
</MythicStyledTooltip>
|
||||
|
||||
}
|
||||
</span>
|
||||
{" / "}
|
||||
<GetOperatorDisplay initialHideUsernameValue={initialHideUsernameValue} task={task}/>
|
||||
{" / "}
|
||||
<MythicStyledTooltip title={"View Callback in separate page"}>
|
||||
<Link style={{wordBreak: "break-all", color: theme.taskPromptTextColor}} underline="always" target="_blank"
|
||||
href={"/new/callbacks/" + task.callback.display_id}>C-{task.callback.display_id}</Link>
|
||||
</MythicStyledTooltip>
|
||||
|
||||
{initialShowHostnameValue ? ` / ${task.callback.host} ` : ''}
|
||||
{initialShowIPValue ? `/ ${ipValue} ` : ''}
|
||||
{initialShowCallbackGroupsValue ? `/ ${task.callback.mythictree_groups.join(', ')} ` : ''}
|
||||
{" / "}
|
||||
{task?.command?.payloadtype?.name}
|
||||
{" / "}
|
||||
{initialShowOPSECBypassUsername && <>
|
||||
{(task?.opsec_pre_bypass_user?.username || task?.opsec_post_bypass_user?.username) && <span
|
||||
style={{display:"inline-flex", whiteSpace: "pre-wrap"}}>
|
||||
{" /"}
|
||||
<MythicStyledTooltip title={"The specified usernames approved OPSEC bypasses for this task"}
|
||||
tooltipStyle={{padding: 0}}>
|
||||
<LockOpenIcon color={"inherit"} style={{height: "15px"}}/>
|
||||
</MythicStyledTooltip>
|
||||
{task?.opsec_pre_bypass_user?.username}
|
||||
{task?.opsec_pre_bypass_user?.username && task?.opsec_post_bypass_user?.username && ", "}
|
||||
{task?.opsec_post_bypass_user?.username}
|
||||
</span>}
|
||||
</>}
|
||||
{" "}
|
||||
<TaskStatusDisplay task={task} theme={theme}/>
|
||||
{task.comment.length > 0 ? (
|
||||
{task.comment.length > 0 &&
|
||||
<span className={classes.column}>
|
||||
{" / "}
|
||||
<IconButton size="small" style={{padding: "0"}}
|
||||
onClick={toggleDisplayComment}><ChatOutlinedIcon fontSize={"small"}/></IconButton>
|
||||
</span>
|
||||
) : null}
|
||||
}
|
||||
{task.comment}
|
||||
</Typography>
|
||||
</Typography>
|
||||
<TaskTagDisplay task={task}/>
|
||||
</div>
|
||||
<div>
|
||||
<div onClick={onLocalClick}>
|
||||
|
||||
<div className={classes.column} onClick={onLocalClick}>
|
||||
<div className={classes.column} >
|
||||
<Badge badgeContent={alertBadges} color="warning" anchorOrigin={{vertical: 'top', horizontal: 'left'}}>
|
||||
{task.tasks.length > 0 && !displayChildren &&
|
||||
<ExpandMoreIcon onClick={toggleDisplayChildren} />
|
||||
@@ -425,7 +440,7 @@ const TaskRow = ({task, filterOptions, me, newlyIssuedTasks, indentLevel, collap
|
||||
"parameterString": onlyParameters,
|
||||
"hideErrors": hideErrors
|
||||
}); */
|
||||
if(hideBrowserTasking && task.tasking_location.includes("browser")){
|
||||
if(hideBrowserTasking && task.tasking_location.includes("_browser")){
|
||||
setShouldDisplay(false);
|
||||
return;
|
||||
}
|
||||
@@ -508,6 +523,10 @@ const TaskRow = ({task, filterOptions, me, newlyIssuedTasks, indentLevel, collap
|
||||
return;
|
||||
}
|
||||
setDisplayChildren(!displayChildren);
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
}, [displayChildren]);
|
||||
useEffect( () => {
|
||||
if(collapseAllRequest > 0){
|
||||
@@ -663,11 +682,14 @@ const TaskRowFlat = ({task, filterOptions, me, onSelectTask, showOnSelectTask, s
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}, [displayChildren]);
|
||||
const onLocalSelectTask = React.useCallback( () => {
|
||||
onSelectTask(task);
|
||||
}, [task]);
|
||||
return (
|
||||
shouldDisplay && (
|
||||
<div style={{marginLeft: (indentLevel * 10) + "px"}}>
|
||||
<TaskLabelFlat me={me} task={task}
|
||||
onSelectTask={() => {onSelectTask(task)}}
|
||||
onSelectTask={onLocalSelectTask}
|
||||
showOnSelectTask={showOnSelectTask}
|
||||
toggleDisplayChildren={toggleDisplayChildren} displayChildren={displayChildren}
|
||||
/>
|
||||
@@ -785,7 +807,7 @@ export const TaskLabelFlat = ({task, me, showOnSelectTask, onSelectTask, graphVi
|
||||
}
|
||||
|
||||
return(
|
||||
<StyledPaper className={task.selected ? classes.root + " selectedTask no-box-shadow" : classes.root}
|
||||
<StyledPaper className={task.selected && showOnSelectTask ? classes.root + " selectedTask no-box-shadow" : classes.root}
|
||||
elevation={5} style={{marginRight: 0, cursor: "pointer"}} id={`taskHeader-${task.id}`}
|
||||
onClick={onClickEntry}
|
||||
>
|
||||
|
||||
@@ -36,6 +36,11 @@ import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen';
|
||||
import CodeOffIcon from '@mui/icons-material/CodeOff';
|
||||
import SettingsTwoToneIcon from '@mui/icons-material/SettingsTwoTone';
|
||||
import {b64DecodeUnicode} from './ResponseDisplay';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {EventTriggerContextSelectDialog} from "../Eventing/EventTriggerContextSelect";
|
||||
import PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwoTone';
|
||||
|
||||
const ReissueTaskMutationGQL = gql`
|
||||
mutation reissueTaskMutation($task_id: Int!){
|
||||
@@ -131,7 +136,7 @@ export const TaskDisplayContainerFlat = ({task, me}) => {
|
||||
}, [searchOutput]);
|
||||
|
||||
return (
|
||||
<div style={{ height: "100%", position: "relative", display: "flex", flexDirection: "column", overflowY: "auto", }}>
|
||||
<div style={{ height: "100%", position: "relative", display: "flex", overflowY: "auto", justifyContent: "flex-end" }}>
|
||||
<ResponseDisplay
|
||||
key={task.id}
|
||||
task={task}
|
||||
@@ -143,7 +148,7 @@ export const TaskDisplayContainerFlat = ({task, me}) => {
|
||||
expand={true}
|
||||
responseRef={responseRef}
|
||||
/>
|
||||
<SpeedDialDisplayGeneric toggleViewBrowserScript={toggleViewBrowserScript}
|
||||
<SideDisplayGeneric toggleViewBrowserScript={toggleViewBrowserScript}
|
||||
toggleSelectAllOutput={toggleSelectAllOutput}
|
||||
toggleOpenSearch={toggleOpenSearch}
|
||||
taskData={task}
|
||||
@@ -179,11 +184,278 @@ export const TaskDisplayContainerConsole = ({task, me}) => {
|
||||
);
|
||||
}
|
||||
|
||||
const SideDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput,
|
||||
toggleOpenSearch, taskData, viewAllOutput, me,
|
||||
responseRef, viewBrowserScript}) => {
|
||||
const theme = useTheme();
|
||||
const [task, setTask] = React.useState(taskData || {});
|
||||
const eventingDataRef = React.useRef({name: "", value: 0});
|
||||
const [openEventingDialog, setOpenEventingDialog] = React.useState(false);
|
||||
const [openTaskTagDialog, setOpenTaskTagDialog] = React.useState(false);
|
||||
const [openCommentDialog, setOpenCommentDialog] = React.useState(false);
|
||||
const [openParametersDialog, setOpenParametersDialog] = React.useState(false);
|
||||
const [openTokenDialog, setOpenTokenDialog] = React.useState(false);
|
||||
const [openStdoutStderrDialog, setOpenStdoutStderrDialog] = React.useState(false);
|
||||
const [openOpsecDialog, setOpenOpsecDialog] = React.useState({open: false, view: "pre"});
|
||||
const [downloadResponses] = useLazyQuery(getAllResponsesLazyQuery, {
|
||||
fetchPolicy: "network-only",
|
||||
onCompleted: (data) => {
|
||||
const output = data.response.reduce( (prev, cur) => {
|
||||
return prev + b64DecodeUnicode(cur.response);
|
||||
}, b64DecodeUnicode(""));
|
||||
downloadFileFromMemory(output, "task_" + task.id + ".txt");
|
||||
},
|
||||
onError: (data) => {
|
||||
|
||||
}
|
||||
});
|
||||
React.useEffect( () => {
|
||||
setTask(taskData);
|
||||
}, [taskData.id, taskData.token, taskData.original_params, taskData.opsec_pre_blocked, taskData.opsec_pre_bypassed, taskData.opsec_post_blocked, taskData.opsec_post_bypassed])
|
||||
const onDownloadResponses = () => {
|
||||
downloadResponses({variables: {task_id: task.id}});
|
||||
};
|
||||
const copyToClipboard = () => {
|
||||
let command = task?.command?.cmd || task.command_name;
|
||||
let result = copyStringToClipboard(command + " " + task.original_params);
|
||||
if(result){
|
||||
snackActions.success("Copied text!");
|
||||
}else{
|
||||
snackActions.error("Failed to copy text");
|
||||
}
|
||||
};
|
||||
const [reissueTask] = useMutation(ReissueTaskMutationGQL, {
|
||||
onCompleted: data => {
|
||||
if(data.reissue_task.status === "success"){
|
||||
snackActions.success("Successfully re-issued task to Mythic");
|
||||
}else{
|
||||
snackActions.error("Failed to re-issue task to Mythic: " + data.reissue_task.error);
|
||||
}
|
||||
},
|
||||
onError: data => {
|
||||
console.log(data);
|
||||
snackActions.error("Failed to re-issue task: " + data);
|
||||
}
|
||||
});
|
||||
const [reissueTaskHandler] = useMutation(ReissueTaskHandlerMutationGQL, {
|
||||
onCompleted: data => {
|
||||
if(data.reissue_task_handler.status === "success"){
|
||||
snackActions.success("Successfully resubmitted task for handling");
|
||||
}else{
|
||||
snackActions.warning("Failed to resubmit task for handling: " + data.reissue_task_handler.error);
|
||||
}
|
||||
|
||||
},
|
||||
onError: data => {
|
||||
console.log(data);
|
||||
snackActions.error("Error resubmitting task for handling: " + data);
|
||||
}
|
||||
});
|
||||
const onDownloadImageClickPng = () => {
|
||||
// we calculate a transform for the nodes so that all nodes are visible
|
||||
// we then overwrite the transform of the `.react-flow__viewport` element
|
||||
// with the style option of the html-to-image library
|
||||
snackActions.info("Saving image to png...");
|
||||
(async () => {
|
||||
const canvas = await html2canvas(responseRef.current);
|
||||
const image = canvas.toDataURL("image/png", 1.0);
|
||||
const fakeLink = window.document.createElement("a");
|
||||
fakeLink.style = "display:none;";
|
||||
fakeLink.download = "task_output.png";
|
||||
|
||||
fakeLink.href = image;
|
||||
|
||||
document.body.appendChild(fakeLink);
|
||||
fakeLink.click();
|
||||
document.body.removeChild(fakeLink);
|
||||
|
||||
fakeLink.remove();
|
||||
|
||||
})();
|
||||
};
|
||||
const onReissueTask = () => {
|
||||
reissueTask({variables: {task_id: task.id}});
|
||||
}
|
||||
const onReissueTaskHandler = () => {
|
||||
reissueTaskHandler({variables: {task_id: task.id}});
|
||||
}
|
||||
const onTriggerEventing = () => {
|
||||
eventingDataRef.current = {
|
||||
name: "task_id",
|
||||
value: task.id
|
||||
};
|
||||
setOpenEventingDialog(true);
|
||||
}
|
||||
return (
|
||||
<div style={{height: "100%"}}>
|
||||
{openTaskTagDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openTaskTagDialog}
|
||||
onClose={()=>{setOpenTaskTagDialog(false);}}
|
||||
innerDialog={<ViewEditTagsDialog me={me} target_object={"task_id"} target_object_id={task.id} onClose={()=>{setOpenTaskTagDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
{openCommentDialog && <MythicDialog fullWidth={true} maxWidth="md" open={openCommentDialog}
|
||||
onClose={()=>{setOpenCommentDialog(false);}}
|
||||
innerDialog={<TaskCommentDialog task_id={task.id} onClose={()=>{setOpenCommentDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
{openParametersDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openParametersDialog}
|
||||
onClose={()=>{setOpenParametersDialog(false);}}
|
||||
innerDialog={<TaskViewParametersDialog task_id={task.id} onClose={()=>{setOpenParametersDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
{openTokenDialog && <MythicDialog fullWidth={true} maxWidth="md" open={openTokenDialog}
|
||||
onClose={()=>{setOpenTokenDialog(false);}}
|
||||
innerDialog={<TaskTokenDialog token_id={task.token === undefined ? 0 : task.token.id} onClose={()=>{setOpenTokenDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
{openOpsecDialog.open && <MythicDialog fullWidth={true} maxWidth="lg" open={openOpsecDialog.open}
|
||||
onClose={()=>{setOpenOpsecDialog({...openOpsecDialog, open: false});}}
|
||||
innerDialog={<TaskOpsecDialog task_id={task.id} view={openOpsecDialog.view} onClose={()=>{setOpenOpsecDialog({...openOpsecDialog, open: false});}} />}
|
||||
/>
|
||||
}
|
||||
|
||||
{openStdoutStderrDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openStdoutStderrDialog}
|
||||
onClose={()=>{setOpenStdoutStderrDialog(false);}}
|
||||
innerDialog={<TaskViewStdoutStderrDialog task_id={task.id} onClose={()=>{setOpenStdoutStderrDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
{openEventingDialog &&
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
maxWidth="xl"
|
||||
open={openEventingDialog}
|
||||
onClose={() => {setOpenEventingDialog(false);}}
|
||||
innerDialog={
|
||||
<EventTriggerContextSelectDialog onClose={() => {setOpenEventingDialog(false);}}
|
||||
triggerContext={eventingDataRef.current}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
<Paper elevation={5} style={{width: "30px", display: "flex", flexDirection: "column", alignItems: "center", overflow: "hidden",
|
||||
backgroundColor: "transparent", height: "100%"}}>
|
||||
<MythicStyledTooltip title={"Toggle BrowserScript"} >
|
||||
<IconButton onClick={toggleViewBrowserScript} style={{paddingBottom: "3px"}}>
|
||||
{viewBrowserScript ? <CodeOffIcon color={"error"} /> : <CodeIcon color={"success"}/>}
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={viewAllOutput ? "View Paginated Output" : "View All Output"}>
|
||||
<IconButton onClick={toggleSelectAllOutput} style={{paddingBottom: "3px"}}>
|
||||
{viewAllOutput ? <CloseFullscreenIcon color={"error"} /> : <FontAwesomeIcon style={{color: theme.palette.success.main}} icon={faExpandArrowsAlt} size="sm" />}
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Search Output"}>
|
||||
<IconButton onClick={toggleOpenSearch} style={{paddingBottom: "3px"}}>
|
||||
<SearchIcon color={"info"} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Download output"}>
|
||||
<IconButton onClick={onDownloadResponses} style={{paddingBottom: "3px"}}>
|
||||
<GetAppIcon color={"success"}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Download screenshot of output"}>
|
||||
<IconButton onClick={onDownloadImageClickPng} style={{paddingBottom: "3px"}}>
|
||||
<InsertPhotoIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Edit Tags"}>
|
||||
<IconButton onClick={()=>{setOpenTaskTagDialog(true)}} style={{paddingBottom: "3px"}}>
|
||||
<LocalOfferOutlinedIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Open Task in New Window"}>
|
||||
<IconButton onClick={()=> {window.open('/new/task/' + task.display_id, "_blank")}} style={{paddingBottom: "3px"}}>
|
||||
<FontAwesomeIcon icon={faExternalLinkAlt} size="sm" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Copy original params to clipboard"}>
|
||||
<IconButton onClick={copyToClipboard} style={{paddingBottom: "3px"}}>
|
||||
<FileCopyOutlinedIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Edit Comment"}>
|
||||
<IconButton onClick={()=>{setOpenCommentDialog(true)}} style={{paddingBottom: "3px"}}>
|
||||
<RateReviewOutlinedIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"View All Parameters And Timestamps"}>
|
||||
<IconButton onClick={()=>{setOpenParametersDialog(true);}} style={{paddingBottom: "3px"}}>
|
||||
<KeyboardIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"View Stdout/Stderr of Task"}>
|
||||
<IconButton onClick={()=>{setOpenStdoutStderrDialog(true);}} style={{paddingBottom: "3px"}}>
|
||||
<FontAwesomeIcon style={{color: theme.palette.error.main}} icon={faExclamationTriangle} size="sm" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Trigger Eventing Based on Task"}>
|
||||
<IconButton onClick={()=>{onTriggerEventing();}} style={{paddingBottom: "3px"}}>
|
||||
<PlayCircleFilledTwoToneIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
{task.opsec_pre_blocked === null ? null : ( task.opsec_pre_bypassed === false ? (
|
||||
<MythicStyledTooltip title={"Submit OPSEC PreCheck Bypass Request"}>
|
||||
<IconButton onClick={()=>{setOpenOpsecDialog({open: true, view: "pre"})}} style={{paddingBottom: "3px"}}>
|
||||
<LockIcon style={{color: theme.palette.error.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
): (
|
||||
<MythicStyledTooltip title={"View OPSEC PreCheck Data"}>
|
||||
<IconButton onClick={()=>{setOpenOpsecDialog({open: true, view: "pre"})}} style={{paddingBottom: "3px"}}>
|
||||
<LockOpenIcon style={{color: theme.palette.success.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
)
|
||||
)
|
||||
}
|
||||
{task.opsec_post_blocked === null ? null : ( task.opsec_post_bypassed === false ? (
|
||||
<MythicStyledTooltip title={"Submit OPSEC PostCheck Bypass Request"}>
|
||||
<IconButton onClick={()=>{setOpenOpsecDialog({open: true, view: "post"})}} style={{paddingBottom: "3px"}}>
|
||||
<LockIcon style={{color: theme.palette.error.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
): (
|
||||
<MythicStyledTooltip title={"View OPSEC PostCheck Data"}>
|
||||
<IconButton onClick={()=>{setOpenOpsecDialog({open: true, view: "post"})}} style={{paddingBottom: "3px"}}>
|
||||
<LockOpenIcon style={{color: theme.palette.success.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
)
|
||||
)
|
||||
}
|
||||
{task.token === null ? null : (
|
||||
<MythicStyledTooltip title={"View Token Information"}>
|
||||
<IconButton onClick={()=>{setOpenTokenDialog(true)}} style={{paddingBottom: "3px"}}>
|
||||
<ConfirmationNumberIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
)}
|
||||
{task.status.toLowerCase().includes("error: container") &&
|
||||
<MythicStyledTooltip title={"Resubmit Tasking"}>
|
||||
<IconButton onClick={onReissueTask} style={{paddingBottom: "3px"}}>
|
||||
<ReplayIcon style={{color: theme.palette.warning.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
{task.status.toLowerCase().includes("error: task") &&
|
||||
<MythicStyledTooltip title={"Resubmit Task Handler"}>
|
||||
<IconButton onClick={onReissueTaskHandler} style={{paddingBottom: "3px"}}>
|
||||
<ReplayIcon style={{color: theme.palette.warning.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
</Paper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput,
|
||||
toggleOpenSearch, taskData, viewAllOutput, me,
|
||||
responseRef, style, fabStyle, viewBrowserScript}) => {
|
||||
const theme = useTheme();
|
||||
const [task, setTask] = React.useState(taskData || {});
|
||||
const eventingDataRef = React.useRef({name: "", value: 0});
|
||||
const [openEventingDialog, setOpenEventingDialog] = React.useState(false);
|
||||
const [openSpeedDial, setOpenSpeedDial] = React.useState(false);
|
||||
const [openTaskTagDialog, setOpenTaskTagDialog] = React.useState(false);
|
||||
const [openCommentDialog, setOpenCommentDialog] = React.useState(false);
|
||||
@@ -275,45 +547,60 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput
|
||||
const onReissueTaskHandler = () => {
|
||||
reissueTaskHandler({variables: {task_id: task.id}});
|
||||
}
|
||||
const onTriggerEventing = () => {
|
||||
eventingDataRef.current = {
|
||||
name: "task_id",
|
||||
value: task.id
|
||||
};
|
||||
setOpenEventingDialog(true);
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Backdrop open={openSpeedDial} onClick={()=>{setOpenSpeedDial(false);}} style={{zIndex: 2, position: "absolute"}}/>
|
||||
{openTaskTagDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="lg" open={openTaskTagDialog}
|
||||
{openTaskTagDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openTaskTagDialog}
|
||||
onClose={()=>{setOpenTaskTagDialog(false);}}
|
||||
innerDialog={<ViewEditTagsDialog me={me} target_object={"task_id"} target_object_id={task.id} onClose={()=>{setOpenTaskTagDialog(false);}} />}
|
||||
/>) : null}
|
||||
{openCommentDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="lg" open={openCommentDialog}
|
||||
/>
|
||||
}
|
||||
{openCommentDialog && <MythicDialog fullWidth={true} maxWidth="md" open={openCommentDialog}
|
||||
onClose={()=>{setOpenCommentDialog(false);}}
|
||||
innerDialog={<TaskCommentDialog task_id={task.id} onClose={()=>{setOpenCommentDialog(false);}} />}
|
||||
/>) : null
|
||||
/>
|
||||
}
|
||||
{openParametersDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="lg" open={openParametersDialog}
|
||||
{openParametersDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openParametersDialog}
|
||||
onClose={()=>{setOpenParametersDialog(false);}}
|
||||
innerDialog={<TaskViewParametersDialog task_id={task.id} onClose={()=>{setOpenParametersDialog(false);}} />}
|
||||
/>) : null
|
||||
/>
|
||||
}
|
||||
{openTokenDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="md" open={openTokenDialog}
|
||||
{openTokenDialog && <MythicDialog fullWidth={true} maxWidth="md" open={openTokenDialog}
|
||||
onClose={()=>{setOpenTokenDialog(false);}}
|
||||
innerDialog={<TaskTokenDialog token_id={task.token === undefined ? 0 : task.token.id} onClose={()=>{setOpenTokenDialog(false);}} />}
|
||||
/>) : null
|
||||
/>
|
||||
}
|
||||
{openOpsecDialog.open ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="md" open={openOpsecDialog.open}
|
||||
{openOpsecDialog.open && <MythicDialog fullWidth={true} maxWidth="lg" open={openOpsecDialog.open}
|
||||
onClose={()=>{setOpenOpsecDialog({...openOpsecDialog, open: false});}}
|
||||
innerDialog={<TaskOpsecDialog task_id={task.id} view={openOpsecDialog.view} onClose={()=>{setOpenOpsecDialog({...openOpsecDialog, open: false});}} />}
|
||||
/>) : null
|
||||
/>
|
||||
}
|
||||
|
||||
{openStdoutStderrDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="lg" open={openStdoutStderrDialog}
|
||||
{openStdoutStderrDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openStdoutStderrDialog}
|
||||
onClose={()=>{setOpenStdoutStderrDialog(false);}}
|
||||
innerDialog={<TaskViewStdoutStderrDialog task_id={task.id} onClose={()=>{setOpenStdoutStderrDialog(false);}} />}
|
||||
/>) : null
|
||||
/>
|
||||
}
|
||||
{openEventingDialog &&
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
maxWidth="xl"
|
||||
open={openEventingDialog}
|
||||
onClose={() => {setOpenEventingDialog(false);}}
|
||||
innerDialog={
|
||||
<EventTriggerContextSelectDialog onClose={() => {setOpenEventingDialog(false);}}
|
||||
triggerContext={eventingDataRef.current}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
<SpeedDial
|
||||
ariaLabel="Task Speeddial"
|
||||
icon={<SettingsTwoToneIcon fontSize={"large"} />}
|
||||
@@ -400,6 +687,13 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput
|
||||
tooltipTitle={"View Stdout/Stderr of Task"}
|
||||
onClick={()=>{setOpenStdoutStderrDialog(true);setOpenSpeedDial(false);}}
|
||||
/>
|
||||
<SpeedDialAction
|
||||
icon={<PlayCircleFilledTwoToneIcon />}
|
||||
arrow
|
||||
tooltipPlacement={"top"}
|
||||
tooltipTitle={"Trigger Eventing Based on Task"}
|
||||
onClick={()=>{onTriggerEventing();setOpenSpeedDial(false);}}
|
||||
/>
|
||||
{task.opsec_pre_blocked === null ? null : ( task.opsec_pre_bypassed === false ? (
|
||||
<SpeedDialAction
|
||||
icon={<LockIcon style={{color: theme.palette.error.main}}/>}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {Backdrop, CircularProgress} from '@mui/material';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import {b64DecodeUnicode} from './ResponseDisplay';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import {MythicDraggableDialogTitle} from "../../MythicComponents/MythicDraggableDialogTitle";
|
||||
|
||||
//if we need to get all the loaded commands for the callback and filter, use this
|
||||
const GetLoadedCommandsQuery = gql`
|
||||
@@ -256,6 +257,7 @@ query getCommandQuery($id: Int!){
|
||||
parameter_group_name
|
||||
display_name
|
||||
cli_name
|
||||
verifier_regex
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -976,7 +978,7 @@ export function TaskParametersDialog(props) {
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">{commandInfo.cmd}'s Parameters</DialogTitle>
|
||||
<MythicDraggableDialogTitle>{commandInfo.cmd}'s Parameters</MythicDraggableDialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}}>
|
||||
<CircularProgress color="inherit" />
|
||||
|
||||
@@ -27,20 +27,20 @@ import MythicStyledTableCell from '../../MythicComponents/MythicTableCell';
|
||||
import {MythicFileContext} from "../../MythicComponents/MythicFileContext";
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import {updateCredentialDeleted} from "../Search/CredentialTable";
|
||||
import CloudUploadTwoToneIcon from '@mui/icons-material/CloudUploadTwoTone';
|
||||
|
||||
export const getDynamicQueryParams = gql`
|
||||
export const getDynamicQueryParamsString = `
|
||||
mutation getDynamicParamsMutation($callback: Int!, $command: String!, $payload_type: String!, $parameter_name: String!, $other_parameters: jsonb){
|
||||
dynamic_query_function(callback: $callback, command: $command, payload_type: $payload_type, parameter_name: $parameter_name, other_parameters: $other_parameters){
|
||||
status
|
||||
error
|
||||
choices
|
||||
parameter_name
|
||||
complex_choices {
|
||||
value
|
||||
display_value
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
export const getDynamicQueryParams = gql`
|
||||
${getDynamicQueryParamsString}
|
||||
`;
|
||||
const parseTypedArrayMutation = gql`
|
||||
mutation parseTypedArrayMutation($callback: Int!, $command: String!, $payload_type: String!, $parameter_name: String!, $input_array: [String!]!){
|
||||
@@ -128,7 +128,9 @@ export function TaskParametersDialogRow(props){
|
||||
if(data.dynamic_query_function.status === "success"){
|
||||
try{
|
||||
let choicesInUse = [];
|
||||
if (data.dynamic_query_function.complex_choices !== null && data.dynamic_query_function.complex_choices.length > 0) {
|
||||
if (data.dynamic_query_function.complex_choices !== null &&
|
||||
data.dynamic_query_function.complex_choices !== undefined &&
|
||||
data.dynamic_query_function.complex_choices.length > 0) {
|
||||
usingDynamicParamComplexChoices.current = true;
|
||||
setChoiceOptions([...data.dynamic_query_function.complex_choices]);
|
||||
choicesInUse = [...data.dynamic_query_function.complex_choices];
|
||||
@@ -209,6 +211,7 @@ export function TaskParametersDialogRow(props){
|
||||
}
|
||||
}
|
||||
}catch(error){
|
||||
console.log(error);
|
||||
setBackdropOpen(false);
|
||||
snackActions.warning("Failed to parse dynamic parameter results");
|
||||
usingDynamicParamComplexChoices.current = false;
|
||||
@@ -557,13 +560,13 @@ export function TaskParametersDialogRow(props){
|
||||
setValue(event.target.checked);
|
||||
props.onChange(props.name, event.target.checked);
|
||||
}
|
||||
const onFileChange = (evt) => {
|
||||
setFileValue({name: evt.target.files[0].name});
|
||||
props.onChange(props.name, evt.target.files[0]);
|
||||
const onFileChange = (newFile) => {
|
||||
setFileValue({name: newFile.name});
|
||||
props.onChange(props.name, newFile);
|
||||
}
|
||||
const onFileMultChange = (evt) => {
|
||||
setFileMultValue([...evt.target.files]);
|
||||
props.onChange(props.name, [...evt.target.files]);
|
||||
const onFileMultChange = (newFiles) => {
|
||||
setFileMultValue([...newFiles]);
|
||||
props.onChange(props.name, [...newFiles]);
|
||||
}
|
||||
const onChangeAgentConnectHost = (event) => {
|
||||
setAgentConnectHost(event.target.value);
|
||||
@@ -741,6 +744,7 @@ export function TaskParametersDialogRow(props){
|
||||
</FormControl>
|
||||
OR
|
||||
<MythicTextField required={props.required} placeholder={"Custom Value"} value={chooseOneCustomValue} multiline={true} maxRows={5}
|
||||
validate={testParameterValues} errorText={"Must match: " + props.verifier_regex}
|
||||
onChange={onChangeTextChooseOneCustom} display="inline-block" onEnter={props.onSubmit} autoFocus={props.autoFocus}
|
||||
name={props.name} marginTop={"5px"}
|
||||
/>
|
||||
@@ -920,28 +924,14 @@ 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>
|
||||
<DragAndDropFileUpload value={fileValue} multiple={false} onChange={onFileChange} />
|
||||
</>
|
||||
|
||||
)
|
||||
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>
|
||||
))
|
||||
}
|
||||
<DragAndDropFileUpload values={fileMultValue} multiple={true} onChange={onFileMultChange} />
|
||||
</>
|
||||
)
|
||||
case "LinkInfo":
|
||||
@@ -1198,15 +1188,16 @@ export function TaskParametersDialogRow(props){
|
||||
return (
|
||||
<TableRow key={"buildparam" + props.id}>
|
||||
<MythicStyledTableCell >
|
||||
<Typography style={{fontWeight: "600"}} >
|
||||
<Typography style={{fontWeight: "600", wordBreak: "break-all"}} >
|
||||
{props.display_name}
|
||||
</Typography>
|
||||
<Typography variant={"body2"} style={{fontSize: theme.typography.pxToRem(15)}}>
|
||||
<Typography variant={"body2"} style={{
|
||||
fontSize: theme.typography.pxToRem(14),
|
||||
wordBreak: "break-all"
|
||||
}}>
|
||||
{props.description}
|
||||
</Typography>
|
||||
{props.required ? (
|
||||
<Typography component="div" color={"warning"}>Required</Typography>
|
||||
) : null }
|
||||
{props.required && <Typography component="div" color={"warning"}>Required</Typography>}
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
{getParameterObject()}
|
||||
@@ -1215,3 +1206,115 @@ export function TaskParametersDialogRow(props){
|
||||
)
|
||||
}
|
||||
|
||||
export const DragAndDropFileUpload = ({value, values, multiple, onChange}) => {
|
||||
const theme = useTheme();
|
||||
const inputRef = React.useRef(null);
|
||||
const [files, setFiles] = React.useState(values ? values : []);
|
||||
const [file, setFile] = React.useState(value ? value : {name: ""});
|
||||
const [isDragging, setIsDragging] = React.useState(false);
|
||||
|
||||
React.useEffect( () => {
|
||||
if(value){
|
||||
setFile(value);
|
||||
}
|
||||
if(values){
|
||||
setFiles(values);
|
||||
}
|
||||
}, [value, values]);
|
||||
const handleDragEnter = () => setIsDragging(true);
|
||||
const handleDragLeave = (event) => {
|
||||
if (event.currentTarget.contains(event.relatedTarget)) {
|
||||
return; // Don't treat as a real leave event
|
||||
}
|
||||
setIsDragging(false);
|
||||
}
|
||||
const handleDrop = (e) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
const droppedFiles = Array.from(e.dataTransfer.files);
|
||||
if(multiple){
|
||||
setFiles(droppedFiles);
|
||||
onChange(droppedFiles)
|
||||
} else {
|
||||
setFile(droppedFiles[0]);
|
||||
onChange(droppedFiles[0]);
|
||||
}
|
||||
};
|
||||
const handleDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
const onFileChange = (evt) => {
|
||||
setFile({name: evt.target.files[0].name});
|
||||
onChange(evt.target.files[0]);
|
||||
}
|
||||
const onFileMultChange = (evt) => {
|
||||
setFiles([...evt.target.files]);
|
||||
onChange(evt.target.files)
|
||||
}
|
||||
const onClick = (e) => {
|
||||
inputRef.current.click();
|
||||
}
|
||||
return (
|
||||
<div
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onClick={onClick}
|
||||
style={{
|
||||
border: isDragging ? `4px dashed ${theme.palette.success.main}` : `4px dashed ${theme.palette.primary.main}`,
|
||||
padding: "20px",
|
||||
textAlign: "center",
|
||||
borderRadius: "10px",
|
||||
cursor: "pointer"
|
||||
//backgroundColor: isDragging ? "#f0f8ff" : "#fff",
|
||||
}}
|
||||
>
|
||||
<input ref={inputRef} onChange={multiple ? onFileMultChange : onFileChange} type="file" hidden multiple={multiple} />
|
||||
<div style={{display: "flex", flexDirection: "column", alignItems: "center"}}>
|
||||
{!multiple && file.name !== "" &&
|
||||
<>
|
||||
<CloudUploadTwoToneIcon fontSize={"large"} color={"success"} />
|
||||
<Typography>
|
||||
Selected:
|
||||
</Typography>
|
||||
<Typography>
|
||||
{!file.legacy && file.name}
|
||||
{file.legacy &&
|
||||
<MythicFileContext agent_file_id={file.name}
|
||||
extraStyles={{ position: "relative", marginLeft: "5px", marginRight: "5px"}} />
|
||||
}
|
||||
</Typography>
|
||||
</>
|
||||
|
||||
}
|
||||
{multiple && files.length > 0 &&
|
||||
<>
|
||||
<CloudUploadTwoToneIcon fontSize={"large"} color={"success"} />
|
||||
<Typography>
|
||||
Selected:
|
||||
</Typography>
|
||||
{files.map( (f, i) => (
|
||||
<div key={i}>
|
||||
{typeof f === "string" && <MythicFileContext agent_file_id={f} />}
|
||||
{typeof f !== "string" && (f.name)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
|
||||
}
|
||||
{file.name === "" && files.length === 0 &&
|
||||
<>
|
||||
<CloudUploadTwoToneIcon fontSize={"large"} color={"success"} />
|
||||
<Typography>
|
||||
Drag and drop files here
|
||||
</Typography>
|
||||
<Typography>
|
||||
Click to open dialog
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ 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 {MythicDraggableDialogTitle} from "../../MythicComponents/MythicDraggableDialogTitle";
|
||||
|
||||
export const allTokenDataFragment = gql`
|
||||
fragment allTokenData on token {
|
||||
@@ -89,7 +89,7 @@ export function TaskTokenDialog(props) {
|
||||
});
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Token Information</DialogTitle>
|
||||
<MythicDraggableDialogTitle >Token Information</MythicDraggableDialogTitle>
|
||||
<TableContainer className="mythicElement">
|
||||
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
|
||||
<TableHead>
|
||||
|
||||
@@ -68,13 +68,14 @@ export function TaskViewParametersDialog(props) {
|
||||
return <div>Error!</div>;
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicModifyStringDialog title={`View Task Parameters And Timestamps`}
|
||||
onClose={props.onClose}
|
||||
maxRows={40}
|
||||
wrap={true}
|
||||
value={comment} />
|
||||
</React.Fragment>
|
||||
<>
|
||||
<MythicModifyStringDialog title={`View Task Parameters And Timestamps`}
|
||||
onClose={props.onClose}
|
||||
maxRows={40}
|
||||
wrap={true}
|
||||
value={comment} />
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ query getParametersQuery ($task_id: Int!) {
|
||||
|
||||
export function TaskViewStdoutStderrDialog(props) {
|
||||
const [comment, setComment] = useState("");
|
||||
const theme = useTheme();
|
||||
const { loading, error } = useQuery(getParametersQuery, {
|
||||
variables: {task_id: props.task_id},
|
||||
onCompleted: data => {
|
||||
|
||||
+5
-1
@@ -14,7 +14,11 @@ import {useTheme} from '@mui/material/styles';
|
||||
|
||||
export function CreatePayloadBuildParametersTable(props){
|
||||
const theme = useTheme();
|
||||
const buildParameters = GetGroupedParameters({buildParameters: props.buildParameters, os: props.os});
|
||||
const buildParameters = GetGroupedParameters({
|
||||
buildParameters: props.buildParameters,
|
||||
os: props.os,
|
||||
c2_name: props.c2_name,
|
||||
});
|
||||
return (
|
||||
<div style={{height: "100%", overflowY: "auto", width: "100%"}}>
|
||||
{buildParameters.map(b => (
|
||||
|
||||
@@ -25,7 +25,7 @@ import { Backdrop } from '@mui/material';
|
||||
import {CircularProgress} from '@mui/material';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import { FormControlLabel } from '@mui/material';
|
||||
import {DragAndDropFileUpload} from "../Callbacks/TaskParametersDialogRow";
|
||||
|
||||
export const getDynamicQueryBuildParameterParams = gql`
|
||||
mutation getDynamicBuildParamsMutation($payload_type: String!, $parameter_name: String!, $selected_os: String!){
|
||||
@@ -37,7 +37,15 @@ mutation getDynamicBuildParamsMutation($payload_type: String!, $parameter_name:
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function isTrue(value){
|
||||
if(typeof value === 'boolean'){
|
||||
return value;
|
||||
}
|
||||
if(typeof value === 'string'){
|
||||
return value.toLowerCase() === 'true' || value.toLowerCase() === 't';
|
||||
}
|
||||
console.log("unknown boolean value", value);
|
||||
}
|
||||
export function CreatePayloadParameter({onChange, parameter_type, default_value, name, required, verifier_regex, id,
|
||||
description, initialValue, choices, trackedValue, instance_name,
|
||||
payload_type, selected_os, dynamic_query_function}){
|
||||
@@ -52,7 +60,7 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
|
||||
const [dictSelectOptions, setDictSelectOptions] = React.useState([]);
|
||||
const [dictSelectOptionsChoice, setDictSelectOptionsChoice] = React.useState("");
|
||||
const [dateValue, setDateValue] = React.useState(dayjs(new Date()));
|
||||
const [arrayValue, setArrayValue] = React.useState([""]);
|
||||
const [arrayValue, setArrayValue] = React.useState([]);
|
||||
const [typedArrayValue, setTypedArrayValue] = React.useState([]);
|
||||
const [fileValue, setFileValue] = React.useState({name: ""});
|
||||
const [fileMultValue, setFileMultValue] = React.useState([]);
|
||||
@@ -175,13 +183,13 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
|
||||
const submitDictChange = (list) => {
|
||||
onChange(name, list, false);
|
||||
};
|
||||
const onFileChange = (evt) => {
|
||||
setFileValue({name: evt.target.files[0].name});
|
||||
onChange(name, evt.target.files[0]);
|
||||
const onFileChange = (newFile) => {
|
||||
setFileValue({name: newFile.name});
|
||||
onChange(name, newFile);
|
||||
}
|
||||
const onFileMultChange = (evt) => {
|
||||
setFileMultValue([...evt.target.files]);
|
||||
onChange(name, [...evt.target.files]);
|
||||
const onFileMultChange = (newFiles) => {
|
||||
setFileMultValue([...newFiles]);
|
||||
onChange(name, [...newFiles]);
|
||||
}
|
||||
useEffect( () => {
|
||||
if( parameter_type === "ChooseOne" || parameter_type === "ChooseOneCustom" ){
|
||||
@@ -467,32 +475,13 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
|
||||
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} style={{display: "inline-block"}}>
|
||||
{typeof f === "string" && <MythicFileContext agent_file_id={f}
|
||||
extraStyles={{bottom: "-10px", position: "relative", marginLeft: "5px", marginRight: "5px"}} />}
|
||||
{typeof f !== "string" && (f.name)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
<DragAndDropFileUpload values={fileMultValue} multiple={true} onChange={onFileMultChange} />
|
||||
</>
|
||||
)
|
||||
case "File":
|
||||
return (
|
||||
<>
|
||||
<Button variant="contained" component="label">
|
||||
{ fileValue.legacy ? "Select New File" : fileValue.name === "" ? "Select File" : fileValue.name }
|
||||
<input onChange={onFileChange} type="file" hidden />
|
||||
</Button>
|
||||
{fileValue.legacy &&
|
||||
<MythicFileContext agent_file_id={fileValue.name}
|
||||
extraStyles={{ position: "relative", marginLeft: "5px", marginRight: "5px"}} />
|
||||
}
|
||||
<DragAndDropFileUpload value={fileValue} multiple={false} onChange={onFileChange} />
|
||||
</>
|
||||
|
||||
)
|
||||
@@ -713,7 +702,7 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
|
||||
return (
|
||||
<Switch
|
||||
color={"info"}
|
||||
checked={Boolean(value)}
|
||||
checked={isTrue(value)}
|
||||
onChange={toggleSwitchValue}
|
||||
inputProps={{ 'aria-label': 'info checkbox' }}
|
||||
/>
|
||||
|
||||
@@ -730,6 +730,18 @@ export const sortByUiPositionThenName = (a, b) => {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
const HideConditionOperandEQ = "eq"
|
||||
const HideConditionOperandNotEQ = "neq"
|
||||
const HideConditionOperandIN = "in"
|
||||
const HideConditionOperandNotIN = "nin"
|
||||
const HideConditionOperandLessThan = "lt"
|
||||
const HideConditionOperandGreaterThan = "gt"
|
||||
const HideConditionOperandLessThanOrEqual = "lte"
|
||||
const HideConditionOperandGreaterThanOrEqual = "gte"
|
||||
const HideConditionOperationStartsWith = "sw"
|
||||
const HideConditionOperationEndsWith = "ew"
|
||||
const HideConditionOperationContains = "co"
|
||||
const HideConditionOperationNotContains = "nco"
|
||||
export const GetGroupedParameters = ({buildParameters, os, c2_name}) => {
|
||||
let groups = buildParameters?.reduce( (prev, cur) => {
|
||||
if(prev.includes(cur?.group_name)){return [...prev]}
|
||||
@@ -756,26 +768,83 @@ export const GetGroupedParameters = ({buildParameters, os, c2_name}) => {
|
||||
for(let l = 0; l < buildParameters.length; l++){
|
||||
if(buildParameters[l].name === buildParameters[i].hide_conditions[k].name){
|
||||
switch(buildParameters[i].hide_conditions[k].operand){
|
||||
case "eq":
|
||||
case HideConditionOperandEQ:
|
||||
if(String(buildParameters[i].hide_conditions[k].value) === String(buildParameters[l].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
break;
|
||||
case "neq":
|
||||
case HideConditionOperandNotEQ:
|
||||
if(String(buildParameters[i].hide_conditions[k].value) !== String(buildParameters[l].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
break;
|
||||
case "in":
|
||||
case HideConditionOperandIN:
|
||||
if(buildParameters[i].hide_conditions[k].choices.includes(buildParameters[l].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
break;
|
||||
case "nin":
|
||||
case HideConditionOperandNotIN:
|
||||
if(!buildParameters[i].hide_conditions[k].choices.includes(buildParameters[l].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
break;
|
||||
case HideConditionOperandLessThan:
|
||||
try{
|
||||
if(parseInt(buildParameters[l].value) < parseInt(buildParameters[i].hide_conditions[k].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
}catch(e){
|
||||
console.log("couldn't parse build parameter value as int", e);
|
||||
}
|
||||
|
||||
break;
|
||||
case HideConditionOperandLessThanOrEqual:
|
||||
try{
|
||||
if(parseInt(buildParameters[l].value) <= parseInt(buildParameters[i].hide_conditions[k].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
}catch(e){
|
||||
console.log("couldn't parse build parameter value as int", e);
|
||||
}
|
||||
break;
|
||||
case HideConditionOperandGreaterThan:
|
||||
try{
|
||||
if(parseInt(buildParameters[l].value) > parseInt(buildParameters[i].hide_conditions[k].value) ){
|
||||
should_hide = true;
|
||||
}
|
||||
}catch(e){
|
||||
console.log("couldn't parse build parameter value as int", e);
|
||||
}
|
||||
break;
|
||||
case HideConditionOperandGreaterThanOrEqual:
|
||||
try{
|
||||
if(parseInt(buildParameters[l].value) >= parseInt(buildParameters[i].hide_conditions[k].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
}catch(e){
|
||||
console.log("couldn't parse build parameter value as int", e);
|
||||
}
|
||||
break;
|
||||
case HideConditionOperationStartsWith:
|
||||
if(String(buildParameters[l].value).startsWith(String(buildParameters[i].hide_conditions[k].value))){
|
||||
should_hide = true;
|
||||
}
|
||||
break;
|
||||
case HideConditionOperationEndsWith:
|
||||
if(String(buildParameters[l].value).endsWith(String(buildParameters[i].hide_conditions[k].value))){
|
||||
should_hide = true;
|
||||
}
|
||||
break;
|
||||
case HideConditionOperationContains:
|
||||
if(buildParameters[l].value.includes(buildParameters[i].hide_conditions[k].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
break;
|
||||
case HideConditionOperationNotContains:
|
||||
if(!buildParameters[l].value.includes(buildParameters[i].hide_conditions[k].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -820,8 +889,6 @@ export const ConfigurationSummary = ({buildParameters, os, c2_name}) => {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
</div>
|
||||
))
|
||||
)
|
||||
|
||||
@@ -39,6 +39,7 @@ query getPayloadTypesC2ProfilesQuery($payloadType: String!, $operation_id: Int!)
|
||||
required
|
||||
verifier_regex
|
||||
choices
|
||||
ui_position
|
||||
}
|
||||
c2profileparametersinstances(where: {instance_name: {_is_null: false}, operation_id: {_eq: $operation_id}}, distinct_on: instance_name, order_by: {instance_name: asc}){
|
||||
instance_name
|
||||
@@ -61,6 +62,7 @@ query getProfileInstanceQuery($name: String!, $operation_id: Int!, $c2_profile_i
|
||||
required
|
||||
verifier_regex
|
||||
choices
|
||||
ui_position
|
||||
c2profile {
|
||||
name
|
||||
}
|
||||
@@ -86,6 +88,7 @@ query getDefaultC2ProfileParameters($c2profile_id: Int!) {
|
||||
required
|
||||
verifier_regex
|
||||
choices
|
||||
ui_position
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,12 +123,12 @@ export const getModifiedC2Params = (c2, c2profileparameters, buildOptions, use_s
|
||||
configuredParam.trackedValue = c2Config.default_value;
|
||||
configuredParam.initialValue = c2Config.default_value;
|
||||
}
|
||||
if(c2Config.dictionary_choices !== undefined){
|
||||
if(c2Config.dictionary_choices !== undefined && c2Config.dictionary_choices !== null){
|
||||
configuredParam.choices = c2Config.dictionary_choices.map(c => {return {...c, value: c.default_value}});
|
||||
configuredParam.value = configuredParam.choices;
|
||||
configuredParam.trackedValue = configuredParam.choices;
|
||||
configuredParam.initialValue = configuredParam.choices;
|
||||
}else if(c2Config.choices !== undefined){
|
||||
}else if(c2Config.choices !== undefined && c2Config.choices !== null){
|
||||
configuredParam.choices = c2Config.choices;
|
||||
if(!configuredParam.choices.includes(configuredParam.default_value)){
|
||||
if(configuredParam.choices.length > 0){
|
||||
@@ -150,7 +153,13 @@ export function Step4C2Profiles(props){
|
||||
onCompleted: data => {
|
||||
const profiles = data.c2profile.map( (c2) => {
|
||||
const parameters = getModifiedC2Params(c2, c2.c2profileparameters, props.buildOptions, false);
|
||||
parameters.sort((a,b) => -b.description.localeCompare(a.description));
|
||||
let hasUiPositions = parameters.filter(p => p.ui_position > 0).length > 0;
|
||||
if(hasUiPositions){
|
||||
parameters.sort((a,b) => a.ui_position > b.ui_position ? 1 : a.ui_position === b.ui_position ? 0 : -1);
|
||||
} else {
|
||||
parameters.sort((a,b) => -b.description.localeCompare(a.description));
|
||||
}
|
||||
|
||||
return {...c2, c2profileparameters: parameters, "selected_instance": "None"};
|
||||
});
|
||||
profiles.sort((a, b) => -b.name.localeCompare(a.name))
|
||||
@@ -285,7 +294,13 @@ export function Step4C2Profiles(props){
|
||||
return inst;
|
||||
});
|
||||
updates = getModifiedC2Params(c2, updates, props.buildOptions, true);
|
||||
updates.sort( (a, b) => a.description < b.description ? -1 : 1);
|
||||
//updates.sort( (a, b) => a.description < b.description ? -1 : 1);
|
||||
let hasUiPositions = updates.filter(p => p.ui_position > 0).length > 0;
|
||||
if(hasUiPositions){
|
||||
updates.sort((a,b) => a.ui_position > b.ui_position ? 1 : a.ui_position === b.ui_position ? 0 : -1);
|
||||
} else {
|
||||
updates.sort((a,b) => -b.description.localeCompare(a.description));
|
||||
}
|
||||
const updatedc2 = includedC2Profiles.map( (curc2, indx) => {
|
||||
if(index === indx){
|
||||
return {...curc2, c2profileparameters: updates, selected_instance: evt.target.value};
|
||||
@@ -310,7 +325,13 @@ export function Step4C2Profiles(props){
|
||||
choices: getDefaultChoices(param)};
|
||||
});
|
||||
updates = getModifiedC2Params(c2, updates, props.buildOptions, false);
|
||||
updates.sort( (a, b) => a.description < b.description ? -1 : 1);
|
||||
let hasUiPositions = updates.filter(p => p.ui_position > 0).length > 0;
|
||||
if(hasUiPositions){
|
||||
updates.sort((a,b) => a.ui_position > b.ui_position ? 1 : a.ui_position === b.ui_position ? 0 : -1);
|
||||
} else {
|
||||
updates.sort((a,b) => -b.description.localeCompare(a.description));
|
||||
}
|
||||
//updates.sort( (a, b) => a.description < b.description ? -1 : 1);
|
||||
const updatedc2 = includedC2Profiles.map( (curc2, indx) => {
|
||||
if(index === indx){
|
||||
return {...curc2, c2profileparameters: updates, selected_instance: 'None'};
|
||||
@@ -662,7 +683,10 @@ const C2ProfileTabs = ({includedC2Profiles, onChange, os, onCloseTab, onChangeCr
|
||||
}
|
||||
</Select>
|
||||
}
|
||||
<CreatePayloadBuildParametersTable instance_name={c.selected_instance} onChange={(n,v,e) => onChange(index, n, v, e)} buildParameters={c.c2profileparameters} os={os} />
|
||||
<CreatePayloadBuildParametersTable instance_name={c.selected_instance} onChange={(n,v,e) => onChange(index, n, v, e)}
|
||||
buildParameters={c.c2profileparameters} os={os}
|
||||
c2_name={c.name}
|
||||
/>
|
||||
</CustomTabPanel>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -81,6 +81,8 @@ export function Step5Build(props){
|
||||
} else if (param.parameter_type === "File") {
|
||||
if (typeof param.value === "string") {
|
||||
buildParameters.push({name: param.name, value: param.value});
|
||||
}else if(param.value.legacy){
|
||||
buildParameters.push({name: param.name, value: param.value.name});
|
||||
} else {
|
||||
const newUUID = await UploadTaskFile(param.value, "Uploaded as build parameter for " + filename);
|
||||
if (newUUID) {
|
||||
@@ -97,6 +99,8 @@ export function Step5Build(props){
|
||||
for(let j = 0; j < param.value.length; j++){
|
||||
if (typeof param.value[j] === "string") {
|
||||
fileMultipleValues.push(param.value[j]);
|
||||
}else if(param.value[j].legacy){
|
||||
fileMultipleValues.push(param.value[j].name);
|
||||
} else {
|
||||
const newUUID = await UploadTaskFile(param.value[j], "Uploaded as build parameter for " + filename);
|
||||
if (newUUID) {
|
||||
@@ -115,7 +119,7 @@ export function Step5Build(props){
|
||||
}
|
||||
}
|
||||
let c2Profiles = [];
|
||||
for(let i = 0; i < props.buildOptions[3].c2.length; i++){
|
||||
for(let i = 0; i < props.buildOptions[3]?.c2?.length; i++){
|
||||
let instanceParam = {};
|
||||
let c2params = GetGroupedParameters({
|
||||
buildParameters: props.buildOptions[3].c2[i].c2profileparameters,
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
import React, { } from 'react';
|
||||
import {useQuery, gql} from '@apollo/client';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Select from '@mui/material/Select';
|
||||
import { CreatePayloadNavigationButtons} from './CreatePayloadNavigationButtons';
|
||||
import {CreatePayloadBuildParametersTable} from './CreatePayloadBuildParametersTable';
|
||||
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!) {
|
||||
payloadtype(where: {supported_os: {_contains: $os}, deleted: {_eq: false}, wrapper: {_eq: true}}, order_by: {name: asc}) {
|
||||
name
|
||||
id
|
||||
file_extension
|
||||
supports_dynamic_loading
|
||||
buildparameters(where: {deleted: {_eq: false} }, order_by: {description: asc}) {
|
||||
default_value
|
||||
description
|
||||
format_string
|
||||
id
|
||||
name
|
||||
parameter_type
|
||||
randomize
|
||||
required
|
||||
verifier_regex
|
||||
choices
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function Step2SelectPayloadType(props){
|
||||
const [selectedPayloadType, setSelectedPayloadType] = React.useState('');
|
||||
const [selectedPayloadTypeID, setSelectedPayloadTypeID] = React.useState(0);
|
||||
const [fileExtension, setFileExtension] = React.useState('');
|
||||
const [payloadTypeParameters, setSelectedPayloadTypeParameters] = React.useState([]);
|
||||
const { loading, error, data } = useQuery(GET_Payload_Types,
|
||||
{variables:{os: props.buildOptions},
|
||||
fetchPolicy: "no-cache",
|
||||
onCompleted: data => {
|
||||
if(data.payloadtype.length > 0){
|
||||
if(props.prevData !== undefined && props.prevData.os === props.buildOptions){
|
||||
|
||||
setSelectedPayloadType(props.prevData.payload_type);
|
||||
setSelectedPayloadTypeID(props.prevData.payload_type_id);
|
||||
setFileExtension(props.prevData.file_extension);
|
||||
const payloadtypedata = data.payloadtype.reduce( (prev, payload) => {
|
||||
if(payload.name === props.prevData.payload_type){
|
||||
const params = payload.buildparameters.map( (param) => {
|
||||
for(let p = 0; p < props.prevData.parameters.length; p++){
|
||||
if(props.prevData.parameters[p]["name"] === param.name){
|
||||
return {...param, error: false,
|
||||
value: props.prevData.parameters[p]["value"],
|
||||
trackedValue: props.prevData.parameters[p]["value"],
|
||||
initialValue: getDefaultValueForType(param),
|
||||
choices: getDefaultChoices(param)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return [...prev, ...params];
|
||||
}
|
||||
return [...prev];
|
||||
}, []);
|
||||
payloadtypedata.sort((a,b) => -b.description.localeCompare(a.description));
|
||||
setSelectedPayloadTypeParameters(payloadtypedata);
|
||||
if(payloadtypedata.length === 0){
|
||||
snackActions.warning("No available payload types exist for the selected OS");
|
||||
}
|
||||
}else{
|
||||
setSelectedPayloadType(data.payloadtype[0].name);
|
||||
setSelectedPayloadTypeID(data.payloadtype[0].id);
|
||||
setFileExtension(data.payloadtype[0].file_extension);
|
||||
const payloadtypedata = data.payloadtype.reduce( (prev, payload) => {
|
||||
if(payload.name === data.payloadtype[0].name){
|
||||
const params = payload.buildparameters.map( (param) => {
|
||||
const initialValue = getDefaultValueForType(param);
|
||||
return {...param, error: false, value: initialValue,
|
||||
trackedValue: initialValue,
|
||||
initialValue: initialValue,
|
||||
choices: getDefaultChoices(param)}
|
||||
});
|
||||
return [...prev, ...params];
|
||||
}
|
||||
return [...prev];
|
||||
}, []);
|
||||
payloadtypedata.sort((a,b) => -b.description.localeCompare(a.description));
|
||||
setSelectedPayloadTypeParameters(payloadtypedata);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const finished = () => {
|
||||
if(selectedPayloadType === ""){
|
||||
snackActions.warning("No payload type selected");
|
||||
return;
|
||||
}
|
||||
props.finished({"payload_type": selectedPayloadType,
|
||||
"parameters": payloadTypeParameters,
|
||||
"payload_type_id": selectedPayloadTypeID,
|
||||
"file_extension": fileExtension,
|
||||
"os": props.buildOptions});
|
||||
}
|
||||
const canceled = () => {
|
||||
props.canceled();
|
||||
}
|
||||
const changePayloadType = (evt) => {
|
||||
setSelectedPayloadType(evt.target.value);
|
||||
const payloadtypedata = data.payloadtype.reduce( (prev, payload) => {
|
||||
if(payload.name === evt.target.value){
|
||||
setFileExtension(payload.file_extension);
|
||||
setSelectedPayloadTypeID(payload.id);
|
||||
const params = payload.buildparameters.map( (param) => {
|
||||
const initialValue = getDefaultValueForType(param);
|
||||
return {...param, error: false, value: initialValue,
|
||||
trackedValue: initialValue,
|
||||
initialValue: initialValue,
|
||||
choices: getDefaultChoices(param)}
|
||||
});
|
||||
return [...prev, ...params];
|
||||
}
|
||||
return [...prev];
|
||||
}, []);
|
||||
payloadtypedata.sort((a,b) => -b.description.localeCompare(a.description));
|
||||
setSelectedPayloadTypeParameters(payloadtypedata);
|
||||
}
|
||||
const onChange = (name, value, error) => {
|
||||
const newParams = payloadTypeParameters.map( (param) => {
|
||||
if(param.name === name){
|
||||
return {...param, value, error}
|
||||
}
|
||||
return {...param};
|
||||
});
|
||||
setSelectedPayloadTypeParameters(newParams);
|
||||
}
|
||||
if (loading) {
|
||||
return <div><CircularProgress /></div>;
|
||||
}
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return <div>Error! {error.message}</div>;
|
||||
}
|
||||
return (
|
||||
<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
|
||||
value={selectedPayloadType}
|
||||
onChange={changePayloadType}
|
||||
>
|
||||
{
|
||||
data.payloadtype.map((opt) => (
|
||||
<MenuItem key={"step2" + opt.name} value={opt.name}>{opt.name}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select><br/>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -101,6 +101,7 @@ export function Step3SelectPayload(props){
|
||||
options.push({name: data.payloadtype_by_pk.wrap_these_payload_types[i].wrapped.name, ...data.payloadtype_by_pk.wrap_these_payload_types[i].wrapped.payloads[j] })
|
||||
}
|
||||
}
|
||||
options = options.sort((a,b) => new Date(a.creation_time) < new Date(b.creation_time) ? 1 : -1)
|
||||
setPayloadOptions(options);
|
||||
setOpenBackdrop(false);
|
||||
}else{
|
||||
|
||||
@@ -103,6 +103,8 @@ export function Step5Build(props){
|
||||
} else if (param.parameter_type === "File") {
|
||||
if (typeof param.value === "string") {
|
||||
buildParameters.push({name: param.name, value: param.value});
|
||||
} else if(param.value.legacy){
|
||||
buildParameters.push({name: param.name, value: param.value.name});
|
||||
} else {
|
||||
const newUUID = await UploadTaskFile(param.value, "Uploaded as build parameter for " + filename);
|
||||
if (newUUID) {
|
||||
@@ -119,6 +121,8 @@ export function Step5Build(props){
|
||||
for(let j = 0; j < param.value.length; j++){
|
||||
if (typeof param.value[j] === "string") {
|
||||
fileMultipleValues.push(param.value[j]);
|
||||
}else if(param.value[j].legacy){
|
||||
fileMultipleValues.push(param.value[j].name);
|
||||
} else {
|
||||
const newUUID = await UploadTaskFile(param.value[j], "Uploaded as build parameter for " + filename);
|
||||
if (newUUID) {
|
||||
|
||||
@@ -93,6 +93,9 @@ mutation UpdateLevelOperationEventLog($id: Int!) {
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const levelOptions = [
|
||||
"All Levels", "warning (unresolved)", "warning (resolved)", "info", "debug", "api", "auth", "agent"
|
||||
];
|
||||
export function EventFeed({}){
|
||||
const [pageData, setPageData] = React.useState({
|
||||
"totalCount": 0,
|
||||
@@ -112,7 +115,30 @@ export function EventFeed({}){
|
||||
updatingPrev[indx] = cur;
|
||||
return [...updatingPrev];
|
||||
}
|
||||
return [...prev, cur];
|
||||
// only add this if this msg fits into the right current view
|
||||
switch(level){
|
||||
case "All Levels":
|
||||
return [...prev, cur];
|
||||
case "warning (unresolved)":
|
||||
if( (cur.warning || cur.level === 'warning') && cur.resolved === false){
|
||||
return [...prev, cur]
|
||||
} else {
|
||||
return [...prev];
|
||||
}
|
||||
case "warning (resolved)":
|
||||
if( (cur.warning || cur.level === 'warning') && cur.resolved === true){
|
||||
return [...prev, cur];
|
||||
} else {
|
||||
return [...prev];
|
||||
}
|
||||
default:
|
||||
if(cur.level === level && !cur.warning){
|
||||
return [...prev, cur];
|
||||
} else {
|
||||
return [...prev];
|
||||
}
|
||||
}
|
||||
//return [...prev, cur];
|
||||
}, [...operationeventlog]);
|
||||
newEvents.sort((a,b) => (a.id > b.id) ? -1 : ((b.id > a.id) ? 1 : 0));
|
||||
setOperationEventLog(newEvents);
|
||||
|
||||
@@ -14,6 +14,9 @@ import MenuItem from '@mui/material/MenuItem';
|
||||
import Select from '@mui/material/Select';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import {alertCount} from "../../../cache";
|
||||
import {levelOptions} from "./EventFeed";
|
||||
import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
|
||||
import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
|
||||
|
||||
const EventList = ({onUpdateLevel, onUpdateResolution, operationeventlog}) => {
|
||||
return (
|
||||
@@ -31,9 +34,7 @@ export function EventFeedTable(props){
|
||||
const theme = useTheme();
|
||||
const [search, setSearch] = React.useState("");
|
||||
const [level, setLevel] = React.useState("info");
|
||||
const levelOptions = [
|
||||
"All Levels", "warning (unresolved)", "warning (resolved)", "info", "debug", "api", "auth", "agent"
|
||||
];
|
||||
|
||||
|
||||
const handleSearchValueChange = (name, value, error) => {
|
||||
setSearch(value);
|
||||
@@ -52,42 +53,41 @@ export function EventFeedTable(props){
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<div style={{display: "flex", flexDirection: "column", height: "100%", maxWidth: "100%", overflowX: "hidden"}}>
|
||||
<Paper elevation={5} style={{backgroundColor: theme.body, marginBottom: "5px", marginRight: "5px"}}>
|
||||
<Grid container spacing={1} style={{ marginTop: "0px"}}>
|
||||
<Grid style={{paddingTop: 0}} size={10}>
|
||||
<MythicTextField placeholder="Search..." value={search} marginBottom={"0px"} marginTop={"0px"}
|
||||
onChange={handleSearchValueChange} onEnter={submitSearch} InputProps={{
|
||||
endAdornment:
|
||||
<React.Fragment>
|
||||
<Tooltip title="Search">
|
||||
<IconButton onClick={submitSearch} size="large"><SearchIcon style={{color: theme.palette.info.main}}/></IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Resolve Viewable Errors">
|
||||
<IconButton onClick={props.resolveViewableErrors} size="large"><AutoFixHighIcon style={{color: theme.palette.success.main}}/></IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Resolve All Errors">
|
||||
<IconButton onClick={props.resolveAllErrors} size="large"><HealingIcon style={{color: theme.palette.success.main}}/></IconButton>
|
||||
</Tooltip>
|
||||
</React.Fragment>,
|
||||
style: {padding: 0}
|
||||
}}/>
|
||||
</Grid>
|
||||
<Grid style={{paddingTop: 0, paddingLeft: 0}} size={2}>
|
||||
<Select
|
||||
style={{width: "100%"}}
|
||||
value={level}
|
||||
onChange={handleLevelValueChange}
|
||||
>
|
||||
{
|
||||
levelOptions.map((opt, i) => (
|
||||
<MenuItem key={"levelFilter" + opt} value={opt}>{opt}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
</Grid>
|
||||
<MythicPageBody>
|
||||
<Grid container spacing={1} style={{ marginTop: "0px"}}>
|
||||
<Grid style={{paddingTop: 0}} size={10}>
|
||||
<MythicTextField placeholder="Search..." value={search} marginBottom={"0px"} marginTop={"0px"}
|
||||
onChange={handleSearchValueChange} onEnter={submitSearch} InputProps={{
|
||||
endAdornment:
|
||||
<React.Fragment>
|
||||
<Tooltip title="Search">
|
||||
<IconButton onClick={submitSearch} size="large"><SearchIcon style={{color: theme.palette.info.main}}/></IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Resolve Viewable Errors">
|
||||
<IconButton onClick={props.resolveViewableErrors} size="large"><AutoFixHighIcon style={{color: theme.palette.success.main}}/></IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Resolve All Errors">
|
||||
<IconButton onClick={props.resolveAllErrors} size="large"><HealingIcon style={{color: theme.palette.success.main}}/></IconButton>
|
||||
</Tooltip>
|
||||
</React.Fragment>,
|
||||
style: {padding: 0}
|
||||
}}/>
|
||||
</Grid>
|
||||
</Paper>
|
||||
<Grid style={{paddingTop: 0, paddingLeft: 0}} size={2}>
|
||||
<Select
|
||||
style={{width: "100%"}}
|
||||
value={level}
|
||||
onChange={handleLevelValueChange}
|
||||
>
|
||||
{
|
||||
levelOptions.map((opt, i) => (
|
||||
<MenuItem key={"levelFilter" + opt} value={opt}>{opt}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<div style={{display: "flex", flexDirection: "column", overflowY: "auto", flexGrow: 1, overflowX: "Hidden"}}>
|
||||
<EventList
|
||||
onUpdateResolution={props.onUpdateResolution}
|
||||
@@ -99,6 +99,6 @@ export function EventFeedTable(props){
|
||||
siblingCount={1} onChange={props.onChangePage} showFirstButton={true} showLastButton={true} style={{padding: "20px"}}/>
|
||||
<Typography style={{paddingLeft: "10px"}}>Total Results: {props.pageData.totalCount}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</MythicPageBody>
|
||||
);
|
||||
}
|
||||
@@ -634,10 +634,10 @@ const CreateEventingStep1 = ({finished, back, first, last, cancel, prevData}) =>
|
||||
<>
|
||||
<div style={{flexGrow: 1, width: "100%", display: "flex", flexDirection: "column", overflowY: "auto"}}>
|
||||
<Table style={{tableLayout: "fixed", width: "100%",}}>
|
||||
<TableHead>
|
||||
<TableHead >
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "10rem"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "10rem"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -686,8 +686,8 @@ const CreateEventingStep1 = ({finished, back, first, last, cancel, prevData}) =>
|
||||
(<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -1073,8 +1073,8 @@ const EventingStepActionDataTaskCreate = ({allSteps, updateStep, index, prevData
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -1264,8 +1264,8 @@ const EventingStepActionDataCustomFunction = ({allSteps, updateStep, index, prev
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -1388,8 +1388,8 @@ const EventingStepActionDataConditionalCheck = ({allSteps, updateStep, index, pr
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -1532,8 +1532,8 @@ const EventingStepActionDataCreatePayload = ({allSteps, updateStep, index, prevD
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -1615,8 +1615,8 @@ const EventingStepActionDataCreateCallback = ({allSteps, updateStep, index, prev
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -1676,8 +1676,8 @@ const EventingStepActionDataTaskIntercept = ({allSteps, updateStep, index, prevD
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -1738,8 +1738,8 @@ const EventingStepActionDataResponseIntercept = ({allSteps, updateStep, index, p
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -1829,8 +1829,8 @@ const EventingStepActionDataCreateAlert = ({allSteps, updateStep, index, prevDat
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -1975,8 +1975,8 @@ const EventingStepActionDataSendWebhook = ({allSteps, updateStep, index, prevDat
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -2137,8 +2137,8 @@ const EventingStep = ({step, allSteps, updateStep, index, step1Data}) => {
|
||||
<Table >
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "15%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "15%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -2337,8 +2337,8 @@ const CreateEventingStep2 = ({finished, back, first, last, cancel, prevData, ste
|
||||
<Table style={{tableLayout: "fixed", width: "100%",}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "3rem"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "3rem"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
|
||||
@@ -13,13 +13,12 @@ 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 {MythicDialog, MythicModifyStringDialog} from "../../MythicComponents/MythicDialog";
|
||||
import OpenInNewTwoToneIcon from '@mui/icons-material/OpenInNewTwoTone';
|
||||
import IosShareIcon from '@mui/icons-material/IosShare';
|
||||
import {copyStringToClipboard} from "../../utilities/Clipboard";
|
||||
import {ipCompare} from "../Callbacks/CallbacksTable";
|
||||
import MythicResizableGrid from "../../MythicComponents/MythicResizableGrid";
|
||||
import {TableFilterDialog} from "../Callbacks/TableFilterDialog";
|
||||
import {CallbacksTableStringCell} from "../Callbacks/CallbacksTableRow";
|
||||
|
||||
const cancelEventGroupInstanceMutation = gql(`
|
||||
@@ -171,15 +170,25 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
|
||||
}
|
||||
|
||||
}, [eventgroups]);
|
||||
const columns = [
|
||||
{key: "id", type: "number", name: "ID", width: 60, enableHiding: false, disableSort: true, disableFilter: true},
|
||||
{key: "status", type: 'string', name: "Status", width: 150, disableSort: true, enableHiding: false},
|
||||
{key: "event_group", type: 'string', name: "Event Group", inMetadata: true, fillWidth: true, disableSort: true, enableHiding: false},
|
||||
{key: "trigger", type: 'string', name: "Trigger", fillWidth: true, disableSort: true, enableHiding: false},
|
||||
{key: "time", type: 'date', name: "Time", width: 300, disableSort: true,},
|
||||
{key: "operator", type: 'string', name: "Operator", inMetadata: true, fillWidth: true, disableSort: true,},
|
||||
{key: "cancel", type: 'string', name: "Action", width: 70, disableSort: true, disableFilter: true},
|
||||
];
|
||||
const columns = React.useMemo(
|
||||
() =>
|
||||
[
|
||||
{key: "id", type: "number", name: "ID", width: 60, enableHiding: false, disableSort: false, disableFilter: true},
|
||||
{key: "status", type: 'string', name: "Status", width: 150, disableSort: true, enableHiding: false},
|
||||
{key: "eventgroup", type: 'string', name: "Event Group", inMetadata: true, fillWidth: true, disableSort: false, enableHiding: false},
|
||||
{key: "trigger", type: 'string', name: "Trigger", fillWidth: true, disableSort: false, enableHiding: false},
|
||||
{key: "time", type: 'date', name: "Time", width: 300, disableSort: true,},
|
||||
{key: "operator", type: 'string', name: "Operator", inMetadata: true, fillWidth: true, disableSort: true,},
|
||||
{key: "cancel", type: 'string', name: "Action", width: 70, disableSort: true, disableFilter: true},
|
||||
]?.reduce( (prev, cur) => {
|
||||
if(filterOptions[cur.key] && String(filterOptions[cur.key]).length > 0){
|
||||
return [...prev, {...cur, filtered: true}];
|
||||
}else{
|
||||
return [...prev, {...cur}];
|
||||
}
|
||||
}, []) || []
|
||||
, [filterOptions]
|
||||
);
|
||||
const onClickHeader = (e, columnIndex) => {
|
||||
const column = columns[columnIndex];
|
||||
if(column.disableSort){
|
||||
@@ -219,7 +228,11 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
|
||||
if (sortData.sortType === 'number' || sortData.sortType === 'size' || sortData.sortType === 'date') {
|
||||
tempData.sort((a, b) => (parseInt(a[sortData.sortKey]) > parseInt(b[sortData.sortKey]) ? 1 : -1));
|
||||
} else if (sortData.sortType === 'string') {
|
||||
tempData.sort((a, b) => (a[sortData.sortKey].toLowerCase() > b[sortData.sortKey].toLowerCase() ? 1 : -1));
|
||||
if(sortData.sortKey === "eventgroup"){
|
||||
tempData.sort((a, b) => (a[sortData.sortKey].name.toLowerCase() > b[sortData.sortKey].name.toLowerCase() ? 1 : -1));
|
||||
} else {
|
||||
tempData.sort((a, b) => (a[sortData.sortKey].toLowerCase() > b[sortData.sortKey].toLowerCase() ? 1 : -1));
|
||||
}
|
||||
} else if (sortData.sortType === "ip") {
|
||||
tempData.sort((a, b) => (ipCompare(a[sortData.sortKey], b[sortData.sortKey])));
|
||||
} else if (sortData.sortType === "array") {
|
||||
@@ -364,11 +377,11 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
|
||||
const sortColumn = columns.findIndex((column) => column.key === sortData.sortKey);
|
||||
const [openContextMenu, setOpenContextMenu] = React.useState(false);
|
||||
const [selectedColumn, setSelectedColumn] = React.useState({});
|
||||
const onSubmitFilterOptions = (newFilterOptions) => {
|
||||
setFilterOptions(newFilterOptions);
|
||||
const onSubmitFilterOptions = (value) => {
|
||||
setFilterOptions({...filterOptions, [selectedColumn.key]: value });
|
||||
}
|
||||
const contextMenuOptions = [{
|
||||
name: 'Filter Column',
|
||||
name: 'Filter Column', type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -402,13 +415,18 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
|
||||
onRowContextMenuClick={onRowContextClick}
|
||||
/>
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xs" open={openContextMenu}
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={<TableFilterDialog
|
||||
selectedColumn={selectedColumn}
|
||||
filterOptions={filterOptions}
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
onClose={()=>{setOpenContextMenu(false);}} />}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={filterOptions[selectedColumn.key]}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
{openDeleteDialog &&
|
||||
|
||||
@@ -66,7 +66,7 @@ mutation eventingManualTrigger($eventgroup_id: Int!){
|
||||
}
|
||||
}
|
||||
`)
|
||||
export function EventGroupTable({selectedEventGroup, me, showInstances, showGraph}) {
|
||||
export function EventGroupTable({selectedEventGroup, me, showInstances, showGraph, height}) {
|
||||
const [openEventStepRender, setOpenEventStepRender] = React.useState(false);
|
||||
const [openEnvView, setOpenEnvView] = React.useState(false);
|
||||
const [openTriggerDataView, setOpenTriggerDataView] = React.useState(false);
|
||||
@@ -141,7 +141,7 @@ export function EventGroupTable({selectedEventGroup, me, showInstances, showGrap
|
||||
triggerManually({variables: {eventgroup_id: selectedEventGroup.id}});
|
||||
}
|
||||
return (
|
||||
<div style={{marginLeft: "5px", display: "flex", overflowY: "auto", flexDirection: "column", height: "100%"}}>
|
||||
<div style={{marginLeft: "5px", display: "flex", overflowY: "auto", flexDirection: "column", height: height || "100%"}}>
|
||||
|
||||
{selectedEventGroup.id === 0 &&
|
||||
<Typography variant={"h4"}><strong>All Eventing Runs</strong></Typography>
|
||||
|
||||
@@ -614,6 +614,7 @@ function EventStepRender({selectedEventGroup, useSuppliedData}) {
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
nodeTypes={nodeTypes}
|
||||
onNodeClick={onPaneClick}
|
||||
onPaneClick={onPaneClick}
|
||||
onNodeContextMenu={onNodeContextMenu}
|
||||
>
|
||||
@@ -622,7 +623,7 @@ function EventStepRender({selectedEventGroup, useSuppliedData}) {
|
||||
EventGroup: {selectedEventGroup.id}
|
||||
</Typography>
|
||||
}
|
||||
<Controls showInteractive={false}>
|
||||
<Controls showInteractive={false} style={{color: "black"}}>
|
||||
</Controls>
|
||||
</ReactFlow>
|
||||
{openContextMenu &&
|
||||
@@ -905,13 +906,14 @@ function EventStepInstanceRender({selectedEventGroupInstance}) {
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
nodeTypes={nodeTypes}
|
||||
onNodeClick={onPaneClick}
|
||||
onPaneClick={onPaneClick}
|
||||
onNodeContextMenu={onNodeContextMenu}
|
||||
>
|
||||
<Typography component={"h1"} style={{marginLeft: "10px"}}>
|
||||
Instance: {selectedEventGroupInstance}
|
||||
</Typography>
|
||||
<Controls showInteractive={false}>
|
||||
<Controls showInteractive={false} style={{color: "black"}}>
|
||||
</Controls>
|
||||
</ReactFlow>
|
||||
{openContextMenu &&
|
||||
|
||||
@@ -19,6 +19,7 @@ import {TextField} from '@mui/material';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import {MeContext} from "../../App";
|
||||
import {EventGroupTable} from "./EventGroupTable";
|
||||
import {MythicDraggableDialogTitle} from "../../MythicComponents/MythicDraggableDialogTitle";
|
||||
|
||||
const triggerManualMutation = gql(`
|
||||
mutation triggerManualMutation($eventgroup_id: Int!, $env_data: jsonb){
|
||||
@@ -203,7 +204,7 @@ export function EventTriggerContextSelectDialog({onClose, triggerContext}) {
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Trigger a workflow</DialogTitle>
|
||||
<MythicDraggableDialogTitle >Trigger a workflow</MythicDraggableDialogTitle>
|
||||
<DialogContent dividers={true} style={{maxHeight: "calc(70vh)"}}>
|
||||
<DialogContentText>
|
||||
Trigger a workflow with selected context and optional additional data.
|
||||
@@ -220,7 +221,7 @@ export function EventTriggerContextSelectDialog({onClose, triggerContext}) {
|
||||
}
|
||||
</Select>
|
||||
{selectedEventGroup.id !== 0 &&
|
||||
<EventGroupTable me={me} selectedEventGroup={selectedEventGroup} showInstances={false} showGraph={false}/>
|
||||
<EventGroupTable me={me} selectedEventGroup={selectedEventGroup} showInstances={false} showGraph={false} height={"unset"}/>
|
||||
}
|
||||
<Table>
|
||||
<TableHead>
|
||||
|
||||
@@ -310,8 +310,10 @@ export function Eventing({me}){
|
||||
/>
|
||||
}
|
||||
<ListItem onClick={() => setSelectedEventGroup({id: 0})}
|
||||
className={selectedEventGroup.id === 0 ? "selectedCallback": ""}
|
||||
style={selectedEventGroup.id === 0 ?
|
||||
{paddingTop: 0, paddingBottom: 0, borderLeft: `5px solid ${theme.palette.info.main}`} :
|
||||
{paddingTop: 0, paddingBottom: 0, borderLeft: `5px solid ${theme.palette.info.main}`,
|
||||
borderRight: `5px solid ${theme.palette.info.main}`} :
|
||||
{paddingTop: 0, paddingBottom: 0}}>
|
||||
<ListItemText primary={"View All Instances"} />
|
||||
</ListItem>
|
||||
@@ -335,8 +337,11 @@ export function Eventing({me}){
|
||||
{eventgroups.map( (e, i) => (
|
||||
(showDeleted || !e.deleted) &&
|
||||
<ListItem key={e.id + e.name} onClick={() => setSelectedEventGroup(e)}
|
||||
className={selectedEventGroup.id === e.id ? "selectedCallback": ""}
|
||||
style={selectedEventGroup.id === e.id ?
|
||||
{paddingTop: 0, paddingBottom: 0, borderLeft: `5px solid ${theme.palette.info.main}`} :
|
||||
{paddingTop: 0, paddingBottom: 0,
|
||||
borderLeft: `5px solid ${theme.palette.info.main}`, borderRight: `5px solid ${theme.palette.info.main}`,
|
||||
} :
|
||||
{paddingTop: 0, paddingBottom: 0}}>
|
||||
{!e.active &&
|
||||
<NotificationsOffTwoToneIcon color={"warning"} />
|
||||
|
||||
@@ -59,8 +59,8 @@ export function ExpandedCallbackSideDetailsTable(props){
|
||||
<Table size="small" style={{tableLayout: "fixed"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{width: "12rem"}}></TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell style={{width: "12rem"}}>Callback Info</TableCell>
|
||||
<TableCell>Value</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody style={{whiteSpace: "pre-wrap", wordBreak: "break-all"}}>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
export const Hasura = () => {
|
||||
return (
|
||||
<iframe height={"100%"} width={"100%"} style={{border:"none"}}
|
||||
src={"/console"} title={"Hasura Console"} />
|
||||
)
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useQuery, gql} from '@apollo/client';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
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 {useTheme} from '@mui/material/styles';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip';
|
||||
|
||||
const GetC2ProfilesAndPayloadTypes = gql`
|
||||
query GetC2AndPayloadType {
|
||||
c2profile(where: {deleted: {_eq: false}}) {
|
||||
name
|
||||
id
|
||||
container_running
|
||||
}
|
||||
payloadtype(where: {deleted: {_eq: false}, agent_type: {_eq: "agent"}}) {
|
||||
name
|
||||
wrapper
|
||||
id
|
||||
container_running
|
||||
payloadtypec2profiles {
|
||||
c2profile {
|
||||
name
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
wrappers: payloadtype(where: {deleted: {_eq: false}, agent_type: {_eq: "agent"}, wrapper: {_eq: true}}) {
|
||||
name
|
||||
wrapper
|
||||
id
|
||||
container_running
|
||||
wrap_these_payload_types {
|
||||
wrapped {
|
||||
wrapper
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function AgentC2Overview(props){
|
||||
const theme = useTheme();
|
||||
const [c2Profiles, setC2Profiles] = React.useState([]);
|
||||
const [payloadTypeRows, setPayloadTypeRows] = React.useState([]);
|
||||
const [payloadTypeRowsNoWrappers, setPayloadTypeRowsNoWrappers] = React.useState([]);
|
||||
const [wrappers, setWrappers] = React.useState([]);
|
||||
const { loading } = useQuery(GetC2ProfilesAndPayloadTypes, {fetchPolicy: "network-only",
|
||||
onCompleted: (data) => {
|
||||
const c2Headers = data.c2profile.map( (c2) => c2.name);
|
||||
const payloadRows = data.payloadtype.map( (payload) => {
|
||||
const payloadc2 = payload.payloadtypec2profiles.map( (c2) => {
|
||||
return c2.c2profile.name;
|
||||
})
|
||||
return {name: payload.name, payloadtypec2profiles: payloadc2, wrapper: payload.wrapper };
|
||||
});
|
||||
|
||||
c2Headers.sort();
|
||||
payloadRows.sort( (a,b) => a.name < b.name ? -1 : 1);
|
||||
const payloadTypeNoWrappers = payloadRows.filter( p => !p.wrapper);
|
||||
const wrapperRows = data.wrappers.map( (payload) => {
|
||||
const wrapped = payload.wrap_these_payload_types.map( (w) => {
|
||||
return w.wrapped.name;
|
||||
});
|
||||
return {name: payload.name, wrapped}
|
||||
});
|
||||
wrapperRows.sort( (a,b) => a.name < b.name ? -1 : 1);
|
||||
setWrappers(wrapperRows);
|
||||
setC2Profiles(c2Headers);
|
||||
setPayloadTypeRows(payloadRows);
|
||||
setPayloadTypeRowsNoWrappers(payloadTypeNoWrappers);
|
||||
},
|
||||
onError: (data) => {
|
||||
|
||||
}
|
||||
});
|
||||
if (loading) {
|
||||
return <LinearProgress />;
|
||||
}
|
||||
return (
|
||||
<div style={{ marginTop: "10px", marginRight: "5px"}}>
|
||||
<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"}}>
|
||||
Agent / C2 Overview
|
||||
</Typography>
|
||||
</Paper>
|
||||
<TableContainer component={Paper} className="mythicElement">
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow hover>
|
||||
<TableCell></TableCell>
|
||||
{c2Profiles.map( (c2) => (
|
||||
<TableCell key={c2}>{c2}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{payloadTypeRowsNoWrappers.map( (payload) => (
|
||||
<TableRow key={payload.name} hover>
|
||||
<TableCell>{payload.name}</TableCell>
|
||||
{c2Profiles.map( (c2) => (
|
||||
<TableCell key={'payload' + c2}>
|
||||
{payload.payloadtypec2profiles.includes(c2) ?
|
||||
<MythicStyledTooltip title={payload.name + " supports " + c2}>
|
||||
<CheckCircleIcon color="success"/>
|
||||
</MythicStyledTooltip>
|
||||
: ""}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{wrappers.length > 0 &&
|
||||
<TableContainer component={Paper} className="mythicElement" style={{marginTop: "10px"}}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow hover>
|
||||
<TableCell></TableCell>
|
||||
{payloadTypeRows.map( (pt) => (
|
||||
<TableCell key={'wrapped' + pt.name}>{pt.name}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{wrappers.map( (payload) => (
|
||||
<TableRow key={'wrapper' + payload.name} hover>
|
||||
<TableCell>{payload.name}</TableCell>
|
||||
{payloadTypeRows.map( (wr) => (
|
||||
<TableCell key={'payload' + wr.name}>
|
||||
{payload.wrapped.includes(wr.name) ?
|
||||
<MythicStyledTooltip title={payload.name + " wraps " + wr.name}>
|
||||
<CheckCircleIcon color="success"/>
|
||||
</MythicStyledTooltip>
|
||||
: ""}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -57,6 +57,7 @@ import {copyStringToClipboard} from "../../utilities/Clipboard";
|
||||
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
|
||||
import {faCopy} from '@fortawesome/free-solid-svg-icons';
|
||||
import MythicTextField from "../../MythicComponents/MythicTextField";
|
||||
import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
|
||||
|
||||
const LeadDashboardQuery = gql`
|
||||
${taskingDataFragment}
|
||||
@@ -1521,13 +1522,7 @@ const Top10RecentScreenshotsDashboardElement = ({me, data, editing, removeElemen
|
||||
<>
|
||||
<TableDataCard title={"Recent Screenshots"}
|
||||
tableHead={
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicTableCell></MythicTableCell>
|
||||
<MythicTableCell></MythicTableCell>
|
||||
<MythicTableCell></MythicTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
null
|
||||
}
|
||||
tableBody={
|
||||
<TableBody>
|
||||
@@ -1594,11 +1589,7 @@ const Top10RecentTasksDashboardElement = ({me, data, editing, removeElement}) =>
|
||||
<>
|
||||
<TableDataCard title={"Recent Tasking"}
|
||||
tableHead={
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicTableCell></MythicTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
null
|
||||
}
|
||||
tableBody={
|
||||
<TableBody>
|
||||
@@ -1714,20 +1705,20 @@ const OperatorDashboard = ({me, setLoading, loading}) => {
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div style={{display: "flex", marginLeft: "5px", marginBottom: "5px", marginTop: "5px"}}>
|
||||
<div style={{display: "flex", marginLeft: "0.5rem", marginBottom: "0.5rem",}}>
|
||||
<ActiveCallbacksDashboardElement me={me} data={analysisData}/>
|
||||
<Top10RecentPayloadsDashboardElement me={me} data={analysisData}/>
|
||||
<Top10RecentWorkflowsDashboardElement me={me} data={analysisData}/>
|
||||
</div>
|
||||
<div style={{display: "flex", marginLeft: "5px", marginBottom: "5px"}}>
|
||||
<div style={{display: "flex", marginLeft: "0.5rem", marginBottom: "0.5rem"}}>
|
||||
<ProxyUsageDashboardElement me={me} data={analysisData}/>
|
||||
<Top10RecentFileDownloadsDashboardElement me={me} data={analysisData}/>
|
||||
<Top10RecentCredentialsDashboardElement me={me} data={analysisData}/>
|
||||
</div>
|
||||
<div style={{display: "flex", marginLeft: "5px", marginBottom: "5px"}}>
|
||||
<div style={{display: "flex", marginLeft: "0.5rem", marginBottom: "0.5rem"}}>
|
||||
<Top10RecentTasksDashboardElement me={me} data={analysisData}/>
|
||||
</div>
|
||||
<div style={{display: "flex", marginLeft: "5px", marginBottom: "5px"}}>
|
||||
<div style={{display: "flex", marginLeft: "0.5rem", marginBottom: "0.5rem"}}>
|
||||
<Top10RecentScreenshotsDashboardElement me={me} data={analysisData}/>
|
||||
<MyOperationsDashboardElement me={me} data={analysisData} reloadDashboard={reloadDashboard}/>
|
||||
</div>
|
||||
@@ -1754,16 +1745,16 @@ const LeadDashboard = ({me, setLoading, loading}) => {
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<div style={{display: "flex", marginLeft: "5px", marginBottom: "5px", marginTop: "5px"}}>
|
||||
<div style={{display: "flex", marginLeft: "0.5rem", marginBottom: "0.5rem",}}>
|
||||
<ActiveCallbacksDashboardElement me={me} data={analysisData} />
|
||||
<Top10CommandStatsDashboardElement me={me} data={analysisData} />
|
||||
<Top10UserContextsDashboardElement me={me} data={analysisData} />
|
||||
<Top10HostContextsDashboardElement me={me} data={analysisData} />
|
||||
</div>
|
||||
<div style={{display: "flex", marginLeft: "5px", marginBottom: "5px"}}>
|
||||
<div style={{display: "flex", marginLeft: "0.5rem", marginBottom: "0.5rem"}}>
|
||||
<ActivityPerDayDashboardElement me={me} data={analysisData} />
|
||||
</div>
|
||||
<div style={{display: "flex", marginLeft: "5px"}}>
|
||||
<div style={{display: "flex", marginLeft: "0.5rem"}}>
|
||||
<TaskStatusDashboardElement me={me} data={analysisData} />
|
||||
<OperatorActivityDashboardElement me={me} data={analysisData} />
|
||||
<Top10ArtifactsDashboardElement me={me} data={analysisData} />
|
||||
@@ -1932,7 +1923,7 @@ const CustomDashboard = ({me, setLoading, loading, editing}) => {
|
||||
<div style={{height: "100%", overflowY: "auto"}}>
|
||||
{dashboards.map((d, i) => (
|
||||
<div key={"dashboardRow" + i}
|
||||
style={{display: "flex", marginLeft: "5px", marginTop: "5px", borderRadius: "4px", }}>
|
||||
style={{display: "flex", marginLeft: "0.5rem", marginBottom: "0.5rem", borderRadius: "5px", }}>
|
||||
{editing &&
|
||||
<div style={{display: "flex", flexDirection: "column", width: "40px", justifyContent: "center"}}>
|
||||
<MythicStyledTooltip title={"Add New Chart To This Row"}>
|
||||
@@ -1976,7 +1967,6 @@ const CustomDashboard = ({me, setLoading, loading, editing}) => {
|
||||
}
|
||||
|
||||
export function CallbacksCard({me}) {
|
||||
const theme = useTheme();
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [editing, setEditing] = React.useState(false);
|
||||
const initialDashboardView = GetMythicSetting({
|
||||
@@ -1992,24 +1982,20 @@ export function CallbacksCard({me}) {
|
||||
setLoading(true);
|
||||
}
|
||||
return (
|
||||
<div style={{width: "100%", height: "100%", display: "flex", flexDirection: "column", position: "relative"}}>
|
||||
<Paper elevation={5} variant={"elevation"} style={{
|
||||
backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,
|
||||
display: "flex", alignItems: "center", justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5" style={{textAlign: "left", display: "inline-block", marginLeft: "10px"}}>
|
||||
<>
|
||||
<MythicPageHeader title={
|
||||
<>
|
||||
Welcome <b>{me.user.username}</b>
|
||||
{me.user.current_operation_id === 0 ? null : (
|
||||
<>
|
||||
{" to"} <b>{me.user.current_operation}</b>'s Dashboard
|
||||
{" to"} <b>{me.user.current_operation}</b>'s Dashboard
|
||||
</>
|
||||
)}
|
||||
|
||||
</Typography>
|
||||
<span>
|
||||
</>
|
||||
}>
|
||||
<div style={{display: "flex", alignItems: "center"}}>
|
||||
<TextField
|
||||
style={{color: "white", marginTop: "5px", width: "10rem"}}
|
||||
style={{color: "white", width: "10rem", margin: "0.5rem"}}
|
||||
InputProps={{style: {color: "white"}}}
|
||||
size={"small"}
|
||||
select={true}
|
||||
@@ -2022,8 +2008,8 @@ export function CallbacksCard({me}) {
|
||||
<MenuItem key={"searchopt" + opt} value={opt}>{opt}</MenuItem>
|
||||
))
|
||||
}
|
||||
</TextField>
|
||||
<MythicStyledTooltip title={"Analyze Operation Data Again"}>
|
||||
</TextField>
|
||||
<MythicStyledTooltip title={"Analyze Operation Data Again"} tooltipStyle={{display: "inline-block"}}>
|
||||
<IconButton style={{color: "white"}} onClick={() => setLoading(true)}>
|
||||
<ReplayIcon />
|
||||
</IconButton>
|
||||
@@ -2035,8 +2021,8 @@ export function CallbacksCard({me}) {
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
</span>
|
||||
</Paper>
|
||||
</div>
|
||||
</MythicPageHeader>
|
||||
<div style={{width: "100%", height: "100%", display: "flex", flexDirection: "column", position: "relative", overflowY: "auto"}}>
|
||||
{loading &&
|
||||
<div style={{
|
||||
@@ -2070,7 +2056,7 @@ export function CallbacksCard({me}) {
|
||||
me={me} loading={loading} setLoading={setLoading} editing={editing}
|
||||
/>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import Paper from "@mui/material/Paper";
|
||||
|
||||
const normalColors = [
|
||||
'#09bdff',
|
||||
@@ -45,16 +46,17 @@ export const PieChartCard = ({
|
||||
const toggleLegend = () => {
|
||||
setShowLegend(!showLegend);
|
||||
}
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<div style={{
|
||||
marginRight: "5px",
|
||||
<Paper variant={"elevation"} elevation={1} style={{
|
||||
marginRight: "0.5rem",
|
||||
width: width,
|
||||
height: "100%",
|
||||
border: "1px solid gray",
|
||||
borderRadius: "4px",
|
||||
border: `1px solid ${theme.borderColor}`,
|
||||
borderRadius: "5px",
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<h3 style={{marginTop: 0, marginLeft: "5px", marginBottom: 0, paddingBottom: 0,}}>
|
||||
<h3 style={{marginTop: 0, marginLeft: "0.5rem", marginBottom: 0, paddingBottom: 0,}}>
|
||||
{editing &&
|
||||
<span>
|
||||
<MythicStyledTooltip title={"Remove element"}>
|
||||
@@ -120,7 +122,7 @@ export const PieChartCard = ({
|
||||
}}>
|
||||
{innerElement}
|
||||
</PieChart>
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
function GaugePointer() {
|
||||
@@ -151,8 +153,7 @@ export const GaugeCard = ({data, width = "100%", additionalStyles, innerElement,
|
||||
right: 10,
|
||||
top: 10,
|
||||
bottom: 10,
|
||||
}, colors = normalColors, onClick, title = "", editing, removeElement, customizeElement
|
||||
}) => {
|
||||
}, colors = normalColors, onClick, title = "", editing, removeElement, customizeElement }) => {
|
||||
const theme = useTheme();
|
||||
const getFillColor = () => {
|
||||
if(data['total'] === 0){return theme.palette.text.disabled}
|
||||
@@ -166,15 +167,15 @@ export const GaugeCard = ({data, width = "100%", additionalStyles, innerElement,
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div style={{
|
||||
marginRight: "5px",
|
||||
<Paper variant={"elevation"} elevation={1} style={{
|
||||
marginRight: "0.5rem",
|
||||
width: width,
|
||||
height: "100%",
|
||||
border: "1px solid gray",
|
||||
border: `1px solid ${theme.borderColor}`,
|
||||
borderRadius: "5px",
|
||||
overflow: "hidden",
|
||||
borderRadius: "4px",
|
||||
}}>
|
||||
<h3 style={{marginTop: 0, marginLeft: "5px", marginBottom: 0, paddingBottom: 0,}}>
|
||||
<h3 style={{marginTop: 0, marginLeft: "0.5rem", marginBottom: 0, paddingBottom: 0,}}>
|
||||
{editing &&
|
||||
<span>
|
||||
<MythicStyledTooltip title={"Remove element"}>
|
||||
@@ -208,21 +209,22 @@ export const GaugeCard = ({data, width = "100%", additionalStyles, innerElement,
|
||||
})}
|
||||
>
|
||||
</Gauge>
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
export const CallbackDataCard = ({mainTitle, secondTitle, mainElement, secondaryElement, width="100%",
|
||||
editing, removeElement}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<div style={{
|
||||
marginRight: "5px",
|
||||
<Paper variant={"elevation"} elevation={1} style={{
|
||||
marginRight: "0.5rem",
|
||||
width: width,
|
||||
height: "100%",
|
||||
border: "1px solid gray",
|
||||
border: `1px solid ${theme.borderColor}`,
|
||||
borderRadius: "5px",
|
||||
overflow: "hidden",
|
||||
borderRadius: "4px",
|
||||
}} >
|
||||
<h5 style={{marginTop: 0, marginLeft: "5px", marginBottom: 0, paddingBottom: 0}}>
|
||||
<h2 style={{marginTop: 0, marginLeft: "0.5rem", marginBottom: 0, paddingBottom: 0}}>
|
||||
{editing &&
|
||||
<span >
|
||||
<MythicStyledTooltip title={"Remove element"}>
|
||||
@@ -233,7 +235,7 @@ export const CallbackDataCard = ({mainTitle, secondTitle, mainElement, secondary
|
||||
</span>
|
||||
}
|
||||
{mainTitle}
|
||||
</h5>
|
||||
</h2>
|
||||
<div style={{height: 180, cursor: "pointer"}}>
|
||||
<MythicStyledTooltip title={"Go to Active Callbacks"}>
|
||||
{mainElement}
|
||||
@@ -243,43 +245,46 @@ export const CallbackDataCard = ({mainTitle, secondTitle, mainElement, secondary
|
||||
{secondaryElement}
|
||||
</MythicStyledTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
export const TableDataCard = ({title, width = "100%", tableHead, tableBody, editing, removeElement, customizeElement}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<div style={{
|
||||
marginRight: "5px",
|
||||
<Paper variant={"elevation"} elevation={1} style={{
|
||||
marginRight: "0.5rem",
|
||||
width: width,
|
||||
height: "100%",
|
||||
border: "1px solid gray",
|
||||
borderRadius: "4px",
|
||||
border: `1px solid ${theme.borderColor}`,
|
||||
borderRadius: "5px",
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<h3 style={{marginTop: 0, marginLeft: "5px", marginBottom: 0, paddingBottom: 0}}>
|
||||
<h3 style={{marginTop: 0, marginLeft: "0.5rem", marginBottom: 0, paddingBottom: 0}}>
|
||||
{editing &&
|
||||
<span >
|
||||
<MythicStyledTooltip title={"Remove element"}>
|
||||
<IconButton onClick={removeElement} >
|
||||
<DeleteIcon color={"error"}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</span>
|
||||
<MythicStyledTooltip title={"Remove element"}>
|
||||
<IconButton onClick={removeElement} >
|
||||
<DeleteIcon color={"error"}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</span>
|
||||
}
|
||||
{title}
|
||||
<span style={{float: "right", marginRight: "5px"}}>
|
||||
<span style={{float: "right", marginRight: "0.5rem"}}>
|
||||
{customizeElement}
|
||||
</span>
|
||||
</h3>
|
||||
<div style={{height: 200, overflowY: "auto"}}>
|
||||
<Table style={{ "maxWidth": "100%", "overflow": "auto"}} stickyHeader size="small">
|
||||
<div style={{height: 200, overflowY: "auto", width: "100%"}}>
|
||||
<Table style={{ maxWidth: "100%", overflow: "auto"}} stickyHeader size="small">
|
||||
{tableHead}
|
||||
{tableBody}
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
export const LineTimeChartCard = ({data, additionalStyles}) => {
|
||||
const theme = useTheme();
|
||||
const [value, setValue] = React.useState([0, 0]);
|
||||
const [range, setRange] = React.useState([0, 0]);
|
||||
React.useEffect( () => {
|
||||
@@ -309,12 +314,12 @@ export const LineTimeChartCard = ({data, additionalStyles}) => {
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div style={{
|
||||
marginBottom: "5px",
|
||||
marginTop: "10px",
|
||||
<Paper variant={"elevation"} elevation={1} style={{
|
||||
marginRight: "0.5rem",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
border: "1px solid gray",
|
||||
border: `1px solid ${theme.borderColor}`,
|
||||
borderRadius: "5px",
|
||||
overflow: "hidden",
|
||||
}} >
|
||||
<Typography variant={"h3"} style={{margin: 0, padding: 0, position: "relative", left: "30%"}}>
|
||||
@@ -369,11 +374,12 @@ export const LineTimeChartCard = ({data, additionalStyles}) => {
|
||||
max={range[1]}
|
||||
sx={{ mt: 2, width: "80%", left: "10%" }}
|
||||
/>
|
||||
</div>
|
||||
</Paper>
|
||||
|
||||
)
|
||||
}
|
||||
export const LineTimeMultiChartCard = ({data, additionalStyles, colors=normalColors, view_utc_time, editing, removeElement, customizeElement}) => {
|
||||
const theme = useTheme();
|
||||
const [value, setValue] = React.useState([0, 0]);
|
||||
const [range, setRange] = React.useState([0, 0]);
|
||||
React.useEffect( () => {
|
||||
@@ -423,13 +429,13 @@ export const LineTimeMultiChartCard = ({data, additionalStyles, colors=normalCol
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div style={{
|
||||
<Paper variant={"elevation"} elevation={1} style={{
|
||||
width: "100%",
|
||||
marginRight: "0.5rem",
|
||||
height: "100%",
|
||||
marginRight: "5px",
|
||||
border: "1px solid gray",
|
||||
border: `1px solid ${theme.borderColor}`,
|
||||
borderRadius: "5px",
|
||||
overflow: "hidden",
|
||||
borderRadius: "4px",
|
||||
}} >
|
||||
<Typography variant={"h3"} style={{margin: 0, padding: 0, position: "relative", left: "30%"}}>
|
||||
{editing &&
|
||||
@@ -500,7 +506,7 @@ export const LineTimeMultiChartCard = ({data, additionalStyles, colors=normalCol
|
||||
max={range[1]}
|
||||
sx={{ mt: 2, width: "80%", left: "10%" }}
|
||||
/>
|
||||
</div>
|
||||
</Paper>
|
||||
|
||||
)
|
||||
}
|
||||
@@ -509,12 +515,14 @@ export const StackedBarChartCard = ({data, labels, title, width="100%", hidden,
|
||||
top: 40,
|
||||
bottom: 10,
|
||||
}}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<div style={{
|
||||
marginRight: "5px",
|
||||
<Paper variant={"elevation"} elevation={1} style={{
|
||||
marginRight: "0.5rem",
|
||||
width: width,
|
||||
height: "100%",
|
||||
border: "1px solid gray",
|
||||
border: `1px solid ${theme.borderColor}`,
|
||||
borderRadius: "5px",
|
||||
overflow: "hidden",
|
||||
}} >
|
||||
<h3 style={{marginTop: 0, marginLeft: "5px", marginBottom: 0, paddingBottom: 0, position: "absolute"}}>
|
||||
@@ -544,6 +552,6 @@ export const StackedBarChartCard = ({data, labels, title, width="100%", hidden,
|
||||
}
|
||||
}
|
||||
}} />
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import {CallbacksCard} from "./CallbacksCard";
|
||||
import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
|
||||
|
||||
|
||||
export function Home({me}) {
|
||||
return (
|
||||
<div style={{width: "100%", height: "100%", display: "flex"}}>
|
||||
<MythicPageBody>
|
||||
<CallbacksCard me={me} />
|
||||
</div>
|
||||
</MythicPageBody>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
export const Jupyter = () => {
|
||||
return (
|
||||
<iframe height={"100%"} width={"100%"} style={{border:"none"}}
|
||||
src={"/jupyter"} title={"Jupyter Notebooks"} />
|
||||
)
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export function LoginForm(props){
|
||||
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";
|
||||
const redirectPath = React.useRef(queryParams.has("redirect") ? queryParams.get("redirect") : "/new");
|
||||
useEffect( () => {
|
||||
if(!isJWTValid()){
|
||||
console.log("calling FailedRefresh on the login page to remove old data");
|
||||
@@ -197,75 +197,78 @@ export function LoginForm(props){
|
||||
return (
|
||||
<div style={{justifyContent: "center", display: "flex"}}>
|
||||
{
|
||||
me.loggedIn && (<Navigate replace to={redirectPath}/>)
|
||||
me.loggedIn && (<Navigate replace to={redirectPath.current}/>)
|
||||
}
|
||||
|
||||
<div style={{backgroundColor: "transparent"}}>
|
||||
<CardContent >
|
||||
<img src={logo} height="400px" alt="Mythic logo"/>
|
||||
{requestField.length === 0 &&
|
||||
<>
|
||||
<MythicTextField name='username' value={username} onChange={onUsernameChange}
|
||||
width={31} debounceDelay={0} showLabel={true} autoComplete={true}/>
|
||||
<MythicTextField name='password' type="password" onEnter={submit} value={password} autoComplete={true}
|
||||
onChange={onPasswordChange} width={31} debounceDelay={0} showLabel={true}/>
|
||||
<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} debounceDelay={0} showLabel={true}/>
|
||||
))}
|
||||
<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 style={{backgroundColor: "transparent"}}>
|
||||
<CardContent >
|
||||
<div style={{height: "400px"}}>
|
||||
<img src={logo} height="400px" alt="Mythic logo"/>
|
||||
</div>
|
||||
|
||||
{requestField.length === 0 &&
|
||||
<>
|
||||
<MythicTextField name='username' value={username} onChange={onUsernameChange}
|
||||
width={31} debounceDelay={0} showLabel={true} autoComplete={true}/>
|
||||
<MythicTextField name='password' type="password" onEnter={submit} value={password} autoComplete={true}
|
||||
onChange={onPasswordChange} width={31} debounceDelay={0} showLabel={true}/>
|
||||
<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} debounceDelay={0} showLabel={true}/>
|
||||
))}
|
||||
<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>
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Backdrop } from '@mui/material';
|
||||
import {CircularProgress} from '@mui/material';
|
||||
import { MythicSelectFromListDialog } from '../../MythicComponents/MythicSelectFromListDialog';
|
||||
import { MythicDialog } from '../../MythicComponents/MythicDialog';
|
||||
import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
|
||||
|
||||
const Get_MITREATTACK = gql`
|
||||
query GetMitreAttack {
|
||||
@@ -367,7 +368,7 @@ export function MitreAttack({me}){
|
||||
getTasksFilteredByTag({variables: {tasks: taskIds}})
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicPageBody>
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={false}>
|
||||
<CircularProgress color="inherit" disableShrink />
|
||||
</Backdrop>
|
||||
@@ -388,7 +389,7 @@ export function MitreAttack({me}){
|
||||
action={"select"} identifier={"cmd"} display={"cmd"}/>}
|
||||
/>
|
||||
}
|
||||
</React.Fragment>
|
||||
</MythicPageBody>
|
||||
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import {CircularProgress} from '@mui/material';
|
||||
import { MythicDisplayTextDialog} from '../../MythicComponents/MythicDisplayTextDialog';
|
||||
import { MythicDialog } from '../../MythicComponents/MythicDialog';
|
||||
import { SelectPayloadTypeDialog } from './SelectPayloadTypeDialog';
|
||||
import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
|
||||
|
||||
|
||||
export function MitreGrid({entries, onGetCommands, onGetTasks, onGetCommandsFiltered, onGetTasksFiltered, onFilterByTags, showCountGrouping}){
|
||||
@@ -23,12 +24,8 @@ export function MitreGrid({entries, onGetCommands, onGetTasks, onGetCommandsFilt
|
||||
const [backdropOpen, setBackdropOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div style={{display: "flex", flexDirection: "column", height: "100%"}}>
|
||||
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,
|
||||
marginBottom: "5px", marginRight: "5px"}} variant={"elevation"}>
|
||||
<Typography variant="h5" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
|
||||
{"MITRE ATT&CK Mappings"}
|
||||
</Typography>
|
||||
<>
|
||||
<MythicPageHeader title={"MITRE ATT&CK Mappings"}>
|
||||
<PoperDropdown onGetCommands={onGetCommands}
|
||||
onGetTasks={onGetTasks}
|
||||
onGetCommandsFiltered={onGetCommandsFiltered}
|
||||
@@ -38,7 +35,7 @@ export function MitreGrid({entries, onGetCommands, onGetTasks, onGetCommandsFilt
|
||||
showCountGrouping={showCountGrouping}
|
||||
entries={entries}
|
||||
/>
|
||||
</Paper>
|
||||
</MythicPageHeader>
|
||||
|
||||
<div style={{display: "flex", flexGrow: 1, overflow: "auto"}}>
|
||||
{backdropOpen && <Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={false}>
|
||||
@@ -47,7 +44,7 @@ export function MitreGrid({entries, onGetCommands, onGetTasks, onGetCommandsFilt
|
||||
}
|
||||
<MitreGridDisplay entries={entries} showCountGrouping={showCountGrouping} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import {MythicConfirmDialog} from '../../MythicComponents/MythicConfirmDialog';
|
||||
import TuneIcon from '@mui/icons-material/Tune';
|
||||
import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
|
||||
|
||||
const newBlockListEntry = gql`
|
||||
mutation newBlockListsEntries($entries: [disabledcommandsprofile_insert_input!]!) {
|
||||
@@ -174,14 +175,11 @@ export function CommandBlockListTable(props){
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main}} variant={"elevation"}>
|
||||
<Typography variant="h5" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
|
||||
Command Block Lists
|
||||
</Typography>
|
||||
<>
|
||||
<MythicPageHeader title={"Command Block Lists"}>
|
||||
<Button size="small"
|
||||
onClick={()=>{setOpenNewDialog(true);}}
|
||||
style={{marginRight: "10px", float: "right", color: "white"}}
|
||||
style={{color: "white", whiteSpace: "nowrap"}}
|
||||
startIcon={<AddCircleIcon color="success" style={{backgroundColor: "white", borderRadius: "10px"}}/>}
|
||||
>New Block List</Button>
|
||||
{openNew &&
|
||||
@@ -191,7 +189,7 @@ export function CommandBlockListTable(props){
|
||||
/>}
|
||||
/>
|
||||
}
|
||||
</Paper>
|
||||
</MythicPageHeader>
|
||||
<TableContainer className="mythicElement">
|
||||
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
|
||||
<TableHead>
|
||||
@@ -211,7 +209,7 @@ export function CommandBlockListTable(props){
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</React.Fragment>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip'
|
||||
import { IconButton } from '@mui/material';
|
||||
import PersonAddIcon from '@mui/icons-material/PersonAdd';
|
||||
import {meState} from "../../../cache";
|
||||
import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
|
||||
|
||||
const newOperatorMutation = gql`
|
||||
mutation NewOperator($username: String!, $password: String!) {
|
||||
@@ -143,105 +144,100 @@ export function OperationTable(props){
|
||||
newOperation({variables: {name: operation_name}})
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main}} variant={"elevation"}>
|
||||
<Typography variant="h5" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
|
||||
Operations
|
||||
</Typography>
|
||||
{showDeleted ? (
|
||||
<MythicStyledTooltip title={"Hide Deleted Operations"} tooltipStyle={{float: "right"}}>
|
||||
<IconButton size="small" style={{float: "right", marginRight: "10px"}}
|
||||
variant="contained" onClick={() => setShowDeleted(!showDeleted)}>
|
||||
<VisibilityIcon />
|
||||
<>
|
||||
<MythicPageHeader title={"Operations"}>
|
||||
<Button size="small" onClick={() => {setOpenNewOperationDialog(true);}}
|
||||
style={{ color: "white", whiteSpace: "nowrap"}}
|
||||
startIcon={<AddCircleIcon color="success" style={{backgroundColor: "white", borderRadius: "10px"}}/>} >
|
||||
New Operation
|
||||
</Button>
|
||||
<MythicStyledTooltip title={"Create new operator"} tooltipStyle={{}}>
|
||||
<IconButton size="small"
|
||||
onClick={()=>{setOpenNewOperatorDialog(true);}}
|
||||
style={{}}
|
||||
variant="contained">
|
||||
<PersonAddIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
|
||||
) : (
|
||||
<MythicStyledTooltip title={"Show Deleted Operations"} tooltipStyle={{float: "right"}}>
|
||||
<IconButton size="small" style={{float: "right", marginRight: "10px"}}
|
||||
variant="contained" onClick={() => setShowDeleted(!showDeleted)} >
|
||||
<VisibilityOffIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
)}
|
||||
<MythicStyledTooltip title={"Create new operator"} tooltipStyle={{float: "right"}}>
|
||||
<IconButton size="small"
|
||||
onClick={()=>{setOpenNewOperatorDialog(true);}}
|
||||
style={{marginRight: "10px", float: "right"}}
|
||||
variant="contained">
|
||||
<PersonAddIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<Button size="small" onClick={() => {setOpenNewOperationDialog(true);}}
|
||||
style={{marginRight: "20px", float: "right", color: "white"}}
|
||||
startIcon={<AddCircleIcon color="success" style={{backgroundColor: "white", borderRadius: "10px"}}/>} >
|
||||
New Operation
|
||||
</Button>
|
||||
{showDeleted ? (
|
||||
<MythicStyledTooltip title={"Hide Deleted Operations"} tooltipStyle={{}}>
|
||||
<IconButton size="small" style={{}}
|
||||
variant="contained" onClick={() => setShowDeleted(!showDeleted)}>
|
||||
<VisibilityIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
) : (
|
||||
<MythicStyledTooltip title={"Show Deleted Operations"} tooltipStyle={{}}>
|
||||
<IconButton size="small" style={{}}
|
||||
variant="contained" onClick={() => setShowDeleted(!showDeleted)} >
|
||||
<VisibilityOffIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
)}
|
||||
{openNewOperator &&
|
||||
<MythicDialog open={openNewOperator}
|
||||
onClose={()=>{setOpenNewOperatorDialog(false);}}
|
||||
innerDialog={<SettingsOperatorDialog title="New Operator" onAccept={onSubmitNewOperator} handleClose={()=>{setOpenNewOperatorDialog(false);}} {...props}/>}
|
||||
/>
|
||||
}
|
||||
{openNewOperation &&
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
open={openNewOperation}
|
||||
onClose={() => {setOpenNewOperationDialog(false);}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog title={"New Operation's Name"}
|
||||
onClose={() => {setOpenNewOperationDialog(false);}}
|
||||
value={""}
|
||||
onSubmit={onSubmitNewOperation}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
||||
{openNewOperator &&
|
||||
<MythicDialog open={openNewOperator}
|
||||
onClose={()=>{setOpenNewOperatorDialog(false);}}
|
||||
innerDialog={<SettingsOperatorDialog title="New Operator" onAccept={onSubmitNewOperator} handleClose={()=>{setOpenNewOperatorDialog(false);}} {...props}/>}
|
||||
/>
|
||||
}
|
||||
{openNewOperation &&
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
open={openNewOperation}
|
||||
onClose={() => {setOpenNewOperationDialog(false);}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog title={"New Operation's Name"}
|
||||
onClose={() => {setOpenNewOperationDialog(false);}}
|
||||
value={""}
|
||||
onSubmit={onSubmitNewOperation}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
||||
</Paper>
|
||||
<TableContainer className="mythicElement">
|
||||
{props.operations.length === 0 &&
|
||||
<div style={{display: "flex", flexDirection: "column", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "50%"
|
||||
}}>
|
||||
{"No Operations available!"}<br/>
|
||||
{"Ask a Mythic admin or operation lead to add you to an operation."}
|
||||
</MythicPageHeader>
|
||||
<TableContainer className="mythicElement">
|
||||
{props.operations.length === 0 &&
|
||||
<div style={{display: "flex", flexDirection: "column", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "50%"
|
||||
}}>
|
||||
{"No Operations available!"}<br/>
|
||||
{"Ask a Mythic admin or operation lead to add you to an operation."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<Table size="small" style={{"tableLayout": "fixed", "overflow": "scroll"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{width: "2rem"}}></TableCell>
|
||||
<TableCell style={{width: "8rem"}}>Configure</TableCell>
|
||||
<TableCell style={{width: "8rem"}}>Operators</TableCell>
|
||||
<TableCell>Operation Name</TableCell>
|
||||
<TableCell>Operation Admin</TableCell>
|
||||
<TableCell style={{width: "8rem"}}>Analysis</TableCell>
|
||||
<TableCell style={{width: "11rem"}}>Operation Status</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{props.operations.map((op) => (
|
||||
showDeleted || !op.deleted ? (
|
||||
<OperationTableRow
|
||||
me={props.me}
|
||||
key={"operation" + op.id}
|
||||
onUpdateOperation={onUpdateOperation}
|
||||
onUpdateCurrentOperation={props.onUpdateCurrentOperation}
|
||||
updateDeleted={props.updateDeleted}
|
||||
{...op} operator={props.operator}
|
||||
/>
|
||||
) : null
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</React.Fragment>
|
||||
}
|
||||
<Table size="small" style={{"tableLayout": "fixed", "overflow": "scroll"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{width: "2rem"}}></TableCell>
|
||||
<TableCell style={{width: "8rem"}}>Configure</TableCell>
|
||||
<TableCell style={{width: "8rem"}}>Operators</TableCell>
|
||||
<TableCell>Operation Name</TableCell>
|
||||
<TableCell>Operation Admin</TableCell>
|
||||
<TableCell style={{width: "8rem"}}>Analysis</TableCell>
|
||||
<TableCell style={{width: "11rem"}}>Operation Status</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{props.operations.map((op) => (
|
||||
showDeleted || !op.deleted ? (
|
||||
<OperationTableRow
|
||||
me={props.me}
|
||||
key={"operation" + op.id}
|
||||
onUpdateOperation={onUpdateOperation}
|
||||
onUpdateCurrentOperation={props.onUpdateCurrentOperation}
|
||||
updateDeleted={props.updateDeleted}
|
||||
{...op} operator={props.operator}
|
||||
/>
|
||||
) : null
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {useQuery, gql, useLazyQuery} from '@apollo/client';
|
||||
import {CommandBlockListTable} from './CommandBlockListTable';
|
||||
import { snackActions } from '../../utilities/Snackbar';
|
||||
import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery";
|
||||
import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
|
||||
|
||||
const GET_Operations = gql`
|
||||
query GetOperations {
|
||||
@@ -114,13 +115,13 @@ export function Operations(props){
|
||||
getBlockLists().then(({data}) => getBlockListsSuccess(data)).catch(({data}) => getBlockListsError(data));
|
||||
}, []);
|
||||
return (
|
||||
<div style={{ height: "100%", display: "flex", flexDirection: "column"}}>
|
||||
<MythicPageBody>
|
||||
<OperationTable operations={operations}
|
||||
onUpdateOperation={onUpdateOperation}
|
||||
onNewOperation={onNewOperation} me={props.me}
|
||||
onUpdateCurrentOperation={onUpdateCurrentOperation}
|
||||
updateDeleted={updateDeleted}/>
|
||||
<CommandBlockListTable blockLists={blockLists} me={props.me} />
|
||||
</div>
|
||||
</MythicPageBody>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import React from 'react';
|
||||
import AttachFileIcon from '@mui/icons-material/AttachFile';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import {useMutation, gql} from '@apollo/client';
|
||||
import {snackActions} from '../../utilities/Snackbar';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import {faFolderOpen} from '@fortawesome/free-solid-svg-icons';
|
||||
import {MythicConfirmDialog} from '../../MythicComponents/MythicConfirmDialog';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import RestoreFromTrashOutlinedIcon from '@mui/icons-material/RestoreFromTrashOutlined';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import MythicTableCell from "../../MythicComponents/MythicTableCell";
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import {MythicDialog} from "../../MythicComponents/MythicDialog";
|
||||
import {C2ProfileListFilesDialog} from "./C2ProfileListFilesDialog";
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
|
||||
const toggleDeleteStatus = gql`
|
||||
mutation toggleCustomBrowserDeleteStatus($custombrowser_id: Int!, $deleted: Boolean!){
|
||||
update_custombrowser_by_pk(pk_columns: {id: $custombrowser_id}, _set: {deleted: $deleted}) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function CustomBrowserRow({service, showDeleted}) {
|
||||
const theme = useTheme();
|
||||
const [openListFilesDialog, setOpenListFilesDialog] = React.useState(false);
|
||||
const [openDelete, setOpenDeleteDialog] = React.useState(false);
|
||||
const [updateDeleted] = useMutation(toggleDeleteStatus, {
|
||||
onCompleted: data => {
|
||||
},
|
||||
onError: error => {
|
||||
if(service.deleted){
|
||||
snackActions.error("Failed to restore browser");
|
||||
} else {
|
||||
snackActions.error("Failed to mark browser as deleted");
|
||||
}
|
||||
}
|
||||
});
|
||||
const onAcceptDelete = () => {
|
||||
updateDeleted({variables: {custombrowser_id: service.id, deleted: !service.deleted}})
|
||||
setOpenDeleteDialog(false);
|
||||
}
|
||||
if(service.deleted && !showDeleted){
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
|
||||
<TableRow >
|
||||
<MythicTableCell>
|
||||
{service.deleted ? (
|
||||
<IconButton size="small" onClick={()=>{setOpenDeleteDialog(true);}} color="success" variant="contained"><RestoreFromTrashOutlinedIcon/></IconButton>
|
||||
) : (
|
||||
<IconButton size="small" onClick={()=>{setOpenDeleteDialog(true);}} color="error" variant="contained"><DeleteIcon/></IconButton>
|
||||
)}
|
||||
{openDelete &&
|
||||
<MythicConfirmDialog onClose={() => {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete}
|
||||
open={openDelete}
|
||||
acceptText={service.deleted ? "Restore" : "Remove"}
|
||||
acceptColor={service.deleted ? "success": "error"} />
|
||||
}
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<FontAwesomeIcon icon={faFolderOpen} style={{width: "60px", height: "60px"}} />
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
{service.name}
|
||||
<Typography variant="body2" component="p" color={service.container_running ? theme.palette.success.main : theme.palette.error.main} >
|
||||
<b>{service.container_running ? "Online" : "Offline"}</b>
|
||||
</Typography>
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
{service.type}
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<Typography variant="body1" component="p">
|
||||
<b>Author:</b> {service.author}
|
||||
</Typography>
|
||||
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
|
||||
<b>Description: </b>{service.description}
|
||||
</Typography>
|
||||
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
|
||||
<b>Version: </b>{service.semver}
|
||||
</Typography>
|
||||
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
|
||||
<b>Export Capabilities: </b>{service.export_function === "" ? "False" : "True"}
|
||||
</Typography>
|
||||
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<MythicStyledTooltip title={service.container_running ? "View Files" : "Unable to view files because container is offline"}>
|
||||
<IconButton
|
||||
disabled={!service.container_running}
|
||||
onClick={()=>{setOpenListFilesDialog(true);}}
|
||||
size="medium">
|
||||
<AttachFileIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</MythicTableCell>
|
||||
<MythicTableCell style={{padding: 0}}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow hover>
|
||||
<MythicTableCell>{"Custom Row Action"}</MythicTableCell>
|
||||
<MythicTableCell>{"Needed UI Feature"}</MythicTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{service.row_actions.map(c => (
|
||||
<TableRow key={c.name} hover>
|
||||
<MythicTableCell>{c.name}</MythicTableCell>
|
||||
<MythicTableCell>{c.ui_feature}</MythicTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow hover>
|
||||
<MythicTableCell>{"Display Table Column"}</MythicTableCell>
|
||||
<MythicTableCell>{"Metadata Key"}</MythicTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{service.columns.map(c => (
|
||||
<TableRow key={c.name} hover>
|
||||
<MythicTableCell>{c.name}</MythicTableCell>
|
||||
<MythicTableCell>{c.key}</MythicTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow hover>
|
||||
<MythicTableCell>{"Extra Table Task Parameter"}</MythicTableCell>
|
||||
<MythicTableCell>{"Parameter Name"}</MythicTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{service.extra_table_inputs.map(c => (
|
||||
<TableRow key={c.name} hover>
|
||||
<MythicTableCell>
|
||||
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
|
||||
<b>{c.display_name}: </b> {c.description}
|
||||
</Typography>
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>{c.name}</MythicTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</MythicTableCell>
|
||||
|
||||
{openListFilesDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openListFilesDialog}
|
||||
onClose={()=>{setOpenListFilesDialog(false);}}
|
||||
innerDialog={<C2ProfileListFilesDialog container_name={service.name} {...service} onClose={()=>{setOpenListFilesDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
</TableRow>
|
||||
|
||||
);
|
||||
}
|
||||
+324
-40
@@ -3,6 +3,7 @@ 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 PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwoTone';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import {useQuery, gql} from '@apollo/client';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
@@ -15,6 +16,10 @@ import MenuBookIcon from '@mui/icons-material/MenuBook';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import {MythicDialog} from "../../MythicComponents/MythicDialog";
|
||||
import Paper from '@mui/material/Paper';
|
||||
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
|
||||
import {getDynamicQueryParamsString} from "../Callbacks/TaskParametersDialogRow";
|
||||
|
||||
const GET_Payload_Details = gql`
|
||||
query GetPayloadDetails($payload_name: String!) {
|
||||
@@ -28,9 +33,40 @@ query GetPayloadDetails($payload_name: String!) {
|
||||
}
|
||||
}
|
||||
`;
|
||||
const GET_CommandParameters = gql`
|
||||
query GetCommandParameters($command_id: Int!) {
|
||||
commandparameters(where: {command_id: {_eq: $command_id}}) {
|
||||
choice_filter_by_command_attributes
|
||||
choices
|
||||
choices_are_all_commands
|
||||
choices_are_loaded_commands
|
||||
cli_name
|
||||
default_value
|
||||
description
|
||||
display_name
|
||||
dynamic_query_function
|
||||
limit_credentials_by_type
|
||||
name
|
||||
parameter_group_name
|
||||
required
|
||||
supported_agent_build_parameters
|
||||
supported_agents
|
||||
type
|
||||
ui_position
|
||||
verifier_regex
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function PayloadTypeCommandDialog({service, payload_name, onClose}) {
|
||||
const [commands, setCommands] = useState([]);
|
||||
const [openScriptDialog, setOpenScriptDialog] = useState({
|
||||
open: false,
|
||||
command_id: 0,
|
||||
command_name: "",
|
||||
});
|
||||
|
||||
const theme = useTheme();
|
||||
const { loading, error } = useQuery(GET_Payload_Details, {
|
||||
variables: {payload_name: payload_name},
|
||||
@@ -47,51 +83,68 @@ export function PayloadTypeCommandDialog({service, payload_name, onClose}) {
|
||||
console.error(error);
|
||||
return <div>Error! {error.message}</div>;
|
||||
}
|
||||
const onClickOpenScriptDialog = (e, command) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setOpenScriptDialog({open: true, command_id: command.id, command_name: command.cmd});
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">{payload_name}'s Commands</DialogTitle>
|
||||
<DialogContentText style={{paddingLeft: "10px"}}>
|
||||
These are the commands associated with this container
|
||||
</DialogContentText>
|
||||
<DialogContent dividers={true} style={{padding: 0}}>
|
||||
<Table size="small" stickyHeader={true} aria-label="details"
|
||||
style={{"tableLayout": "fixed", "overflowWrap": "break-word", overflowY:"auto", width: "100%", height: "100%"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{width: "20%"}}>Command</TableCell>
|
||||
<TableCell style={{width: "6rem"}}>Version</TableCell>
|
||||
<TableCell style={{width: "5rem"}}>Admin</TableCell>
|
||||
<TableCell style={{width: "5rem"}}>Docs</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{
|
||||
commands.map((param) => (
|
||||
<TableRow key={"command" + param.id} hover style={{backgroundColor: param.deleted? theme.palette.action.disabledBackground : ''}}>
|
||||
<TableCell>
|
||||
<Typography style={{textDecoration: param.deleted ? 'line-through' : ''}}>
|
||||
{param.cmd}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{param.version}</TableCell>
|
||||
<TableCell>{param.needs_admin ? "True" : "False"}</TableCell>
|
||||
<TableCell>
|
||||
<IconButton
|
||||
href={service.wrapper ? "/docs/wrappers/" + service.name : "/docs/agents/" + service.name}
|
||||
target="_blank"
|
||||
size="large">
|
||||
<MenuBookIcon/>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<TableCell>{param.description}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Table size="small" stickyHeader={true} aria-label="details"
|
||||
style={{"tableLayout": "fixed", "overflowWrap": "break-word", overflowY:"auto", width: "100%", height: "100%"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{width: "20%"}}>Command</TableCell>
|
||||
<TableCell style={{width: "6rem"}}>Version</TableCell>
|
||||
<TableCell style={{width: "5rem"}}>Docs</TableCell>
|
||||
<TableCell style={{width: "5rem"}}>Script</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{
|
||||
commands.map((param) => (
|
||||
<TableRow key={"command" + param.id} hover style={{backgroundColor: param.deleted? theme.palette.action.disabledBackground : ''}}>
|
||||
<TableCell>
|
||||
<Typography style={{textDecoration: param.deleted ? 'line-through' : ''}}>
|
||||
{param.cmd}
|
||||
</Typography>
|
||||
{param.needs_admin && <Typography style={{fontWeight: 600}} color={"warning"}>
|
||||
{"Needs Admin"}
|
||||
</Typography>}
|
||||
</TableCell>
|
||||
<TableCell>{param.version}</TableCell>
|
||||
<TableCell>
|
||||
<IconButton
|
||||
href={service.wrapper ? "/docs/wrappers/" + service.name : "/docs/agents/" + service.name}
|
||||
target="_blank"
|
||||
size="large">
|
||||
<MenuBookIcon/>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<IconButton onClick={(e) => onClickOpenScriptDialog(e, param)} >
|
||||
<PlayCircleFilledTwoToneIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<TableCell>{param.description}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{openScriptDialog.open &&
|
||||
<MythicDialog fullWidth={true} maxWidth="lg" open={openScriptDialog.open}
|
||||
onClose={()=>{setOpenScriptDialog({open: false, command_id: 0});}}
|
||||
innerDialog={
|
||||
<ScriptingCommandDialog command_id={openScriptDialog.command_id} command_name={openScriptDialog.command_name}
|
||||
onClose={()=>{setOpenScriptDialog({open: false, command_id: 0});}}
|
||||
/>
|
||||
}
|
||||
/>}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" onClick={onClose} color="primary">
|
||||
@@ -102,3 +155,234 @@ export function PayloadTypeCommandDialog({service, payload_name, onClose}) {
|
||||
);
|
||||
}
|
||||
|
||||
const exampleAgentConnect = `
|
||||
"host":"hostname where remote payload/callback is running",
|
||||
"agent_uuid":"payload uuid if trying to connect to payload",
|
||||
"c2_profile":{
|
||||
"name":"name of c2 profile",
|
||||
"parameters":{
|
||||
"parameter name":"parameter value",
|
||||
},
|
||||
"callback_uuid":"callback uuid if trying to connect to callback"
|
||||
}
|
||||
`;
|
||||
const exampleUploadFile = (commandName, parameterName) => `newFileID = await mythic.register_file(
|
||||
mythic=mythic_instance, filename="test.txt", contents=b"this is a test"
|
||||
)
|
||||
status = await mythic.issue_task(
|
||||
mythic=mythic_instance,
|
||||
command_name="${commandName}",
|
||||
parameters={"${parameterName}": newFileID},
|
||||
callback_display_id=1,
|
||||
)
|
||||
`;
|
||||
const exampleCredentialJson = `{
|
||||
"account":"[email protected]",
|
||||
"comment":"",
|
||||
"credential":"doIFO<..snip...>FM",
|
||||
"realm":"SEVENKINGDOMS.LOCAL",
|
||||
"type":"ticket"
|
||||
}
|
||||
`;
|
||||
export function ScriptingCommandDialog({command_id, command_name, onClose}){
|
||||
const [params, setParams] = useState([]);
|
||||
const theme = useTheme();
|
||||
useQuery(GET_CommandParameters, {
|
||||
variables: {command_id: command_id},
|
||||
onCompleted: data => {
|
||||
const grouped = data.commandparameters.reduce( (prev, cur) => {
|
||||
if(prev[cur.parameter_group_name] === undefined){
|
||||
prev[cur.parameter_group_name] = [cur];
|
||||
return prev;
|
||||
}
|
||||
prev[cur.parameter_group_name].push(cur);
|
||||
prev[cur.parameter_group_name].sort((a, b) => {return a.ui_position < b.ui_position ? -1 : 1});
|
||||
return prev;
|
||||
}, {});
|
||||
let groupedArray = [];
|
||||
for(const key in grouped){
|
||||
groupedArray.push(grouped[key]);
|
||||
}
|
||||
setParams(groupedArray);
|
||||
console.log(groupedArray);
|
||||
}
|
||||
});
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">{command_name}'s Parameters</DialogTitle>
|
||||
<DialogContentText>
|
||||
<Typography component={"span"} style={{marginLeft: "10px", display: "block"}}>
|
||||
Each "Parameter Group" is one way to issue this task. These "Parameter Groups" allow a single task to offer a wider range of tasking options with similar code paths without creating new commands for each option and by cleaning up the total parameter options to make it less confusing.
|
||||
</Typography>
|
||||
<Typography component={"span"} style={{marginLeft: "10px", display: "block"}}>
|
||||
<b>Note:</b> Parameters with "Required" must have a value supplied by you, but everything else has a "Default Value" that will be supplied if you choose not to submit one.
|
||||
</Typography>
|
||||
</DialogContentText>
|
||||
<DialogContent dividers={true} style={{padding: 0}}>
|
||||
{params.map( (paramGroup) => (
|
||||
<div key={paramGroup[0].parameter_group_name}>
|
||||
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", }} variant={"elevation"}>
|
||||
<Typography variant="h6" style={{textAlign: "left", display: "inline-block", marginLeft: "20px", color: theme.pageHeaderColor}}>
|
||||
Parameter Group: {paramGroup[0].parameter_group_name}
|
||||
</Typography>
|
||||
</Paper>
|
||||
<Table style={{tableLayout: "fixed"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{width: "40%"}}>Name</TableCell>
|
||||
<TableCell>Value</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{paramGroup.map( (param) => (
|
||||
<TableRow key={param.id}>
|
||||
<MythicStyledTableCell >
|
||||
<Typography style={{fontWeight: "600", wordBreak: "break-all"}} >
|
||||
{param.name}
|
||||
</Typography>
|
||||
<Typography variant={"body2"} style={{
|
||||
fontSize: theme.typography.pxToRem(14),
|
||||
wordBreak: "break-all"
|
||||
}}>
|
||||
{param.description}
|
||||
</Typography>
|
||||
{param.required && <Typography component="div" color={"warning"}>Required</Typography>}
|
||||
</MythicStyledTableCell>
|
||||
<TableCell style={{whiteSpace: "pre-wrap"}}>
|
||||
<Typography>
|
||||
<b>Value Type:</b> {param.type}
|
||||
</Typography>
|
||||
{param.verifier_regex !== "" &&
|
||||
<Typography>
|
||||
Note: Value must match the following regex: {param.verifier_regex}
|
||||
</Typography>
|
||||
}
|
||||
<Typography>
|
||||
<b>Default Value:</b> {param.default_value}
|
||||
</Typography>
|
||||
{param.choices.length > 0 &&
|
||||
<Typography>
|
||||
Choose from the following: {param.choices.join(", ")}
|
||||
</Typography>
|
||||
}
|
||||
{param.choices_are_all_commands &&
|
||||
<Typography>
|
||||
<b>Note:</b> Provide any command name
|
||||
</Typography>
|
||||
}
|
||||
{param.choices_are_loaded_commands &&
|
||||
<Typography>
|
||||
<b>Note:</b> Provide any currently loaded command name
|
||||
</Typography>
|
||||
}
|
||||
{param.dynamic_query_function !== "" &&
|
||||
<Typography>
|
||||
<b>Note:</b> This command in the UI will dynamically give you a list of options to choose from either through the modal or via tab complete.
|
||||
The following mutation can be used to dynamically generate the same choices. In this case, "other_parameters", are just the values of the other parameters you've set for this command in case they offer meaningful context for this dynamic function. They are provided as a standard dictionary (not a string)
|
||||
<div className={"code-box"}>
|
||||
<code>
|
||||
{getDynamicQueryParamsString}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
</Typography>
|
||||
}
|
||||
{param.type === "File" &&
|
||||
<Typography>
|
||||
<b>Note:</b> This parameter expects a file's AgentFileID (UUID) value. When issuing this task through the UI, you will be prompted to select a file from your file system to upload.
|
||||
When scripting, you should upload a new file first and provide the returned AgentFileID here.<br/>
|
||||
<div className={"code-box"}>
|
||||
<code >
|
||||
{exampleUploadFile(command_name, param.name)}
|
||||
</code>
|
||||
</div>
|
||||
</Typography>
|
||||
}
|
||||
{param.type === "TypedArray" &&
|
||||
<Typography>
|
||||
<b>Note:</b> TypedArrays are arrays of strings that also carry along a "type" identifier with it. This is expressed in scripting and the code as an array of arrays.
|
||||
In the UI, tab complete or the modal will show you how to fill this out.
|
||||
When scripting, you can provide this nested array explicitly:
|
||||
<div className={"code-box"}>
|
||||
<code>
|
||||
[ ["{param.choices[0]}", "test"], ["{param.choices[0]}", "values"] ]
|
||||
</code>
|
||||
</div>
|
||||
</Typography>
|
||||
}
|
||||
{param.type === "AgentConnect" &&
|
||||
<Typography>
|
||||
<b>Note:</b> This parameter type is more complex and is meant to provide a handy way in the UI to allow an operator to select a payload or callback to connect to. In the UI, this dynamically resolves hosts and their payloads/callbacks to list out their P2P C2 profile options so that you can link up without remember things like randomized named pipe values.
|
||||
You can do the same thing programmatically, but you'll have to fetch the pieces of information yourself and set the value as the resulting dictionary:
|
||||
<div className={"code-box"}>
|
||||
<code>
|
||||
{exampleAgentConnect}
|
||||
</code>
|
||||
</div>
|
||||
</Typography>
|
||||
}
|
||||
{param.type === "LinkInfo" &&
|
||||
<Typography>
|
||||
<b>Note:</b> This parameter type is more complex and is meant to provide a handy way in the UI to allow an operator to select an existing P2P link for either unlinking or relinking. The UI dynamically resolves existing "callbackgraphedge" values and their info.
|
||||
You can do the same thing programmatically, but you'll have to fetch the pieces of information yourself and set the value as the resulting dictionary. You'll notice that the data is the exact same as if the type was "AgentConnect", it's just displayed in a different format in the UI.
|
||||
<div className={"code-box"}>
|
||||
<code>
|
||||
{exampleAgentConnect}
|
||||
</code>
|
||||
</div>
|
||||
</Typography>
|
||||
}
|
||||
{param.type === "PayloadList" &&
|
||||
<>
|
||||
<Typography>
|
||||
<b>Note:</b> This parameter simply supplies a list of payloads for the user to select. From here, the actual value that's passed along is just the UUID of the payload the user selected.
|
||||
</Typography>
|
||||
{param.supported_agents.length > 0 &&
|
||||
<Typography>
|
||||
<b>Note:</b> This command has an explicit list of agents it supports; this is typically done in conjunction with the "PayloadList" type to help reduce the options to only valid ones.
|
||||
<br/>The supported agents are: {param.supported_agents.join(", ")}
|
||||
</Typography>
|
||||
}
|
||||
{Object.keys(param.supported_agent_build_parameters).length > 0 &&
|
||||
<Typography>
|
||||
<b>Note:</b> This command has an explicit list of build parameter values that must be set for them to be considered valid.
|
||||
<br/>The supported agents and build parameter values are: {JSON.stringify(param.supported_agent_build_parameters, null, 2)}
|
||||
</Typography>
|
||||
}
|
||||
</>
|
||||
}
|
||||
{param.type === "CredentialJson" &&
|
||||
<>
|
||||
<Typography>
|
||||
<b>Note:</b> This parameter type allows the user to explicitly select a credential that exists in Mythic's credential store and pass along all of that data to the command.
|
||||
The value supplied here is the actual JSON credential data like follows:
|
||||
<div className={"code-box"}>
|
||||
<code>{exampleCredentialJson}</code>
|
||||
</div>
|
||||
</Typography>
|
||||
{param.limit_credentials_by_type.length > 0 &&
|
||||
<Typography>
|
||||
<b>Note:</b> This command limits the credential to those with the following types: {param.limit_credentials_by_type.join(", ")}
|
||||
</Typography>
|
||||
}
|
||||
</>
|
||||
|
||||
}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
</div>
|
||||
))}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" onClick={onClose} color="primary">
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
+109
-14
@@ -20,6 +20,9 @@ import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import { IconButton } from '@mui/material';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import {CustomBrowserRow} from "./CustomBrowserCard";
|
||||
import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
|
||||
import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
|
||||
|
||||
const SUB_Payload_Types = gql`
|
||||
subscription getPayloadTypesSubscription {
|
||||
@@ -80,6 +83,7 @@ subscription getTranslationContainersSubscription {
|
||||
deleted
|
||||
description
|
||||
author
|
||||
semver
|
||||
payloadtypes(order_by: {name: asc}) {
|
||||
name
|
||||
deleted
|
||||
@@ -98,6 +102,25 @@ subscription ConsumingContainer{
|
||||
container_running
|
||||
deleted
|
||||
subscriptions
|
||||
semver
|
||||
}
|
||||
}
|
||||
`;
|
||||
const SUB_Custom_Browsers = gql`
|
||||
subscription CustomBrowser{
|
||||
custombrowser(order_by: {name: asc}) {
|
||||
id
|
||||
name
|
||||
description
|
||||
type
|
||||
container_running
|
||||
deleted
|
||||
columns
|
||||
export_function
|
||||
row_actions
|
||||
extra_table_inputs
|
||||
author
|
||||
semver
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -115,7 +138,7 @@ function useCustomSubscription({customSubscription, dataKey}){
|
||||
return customData
|
||||
}
|
||||
|
||||
const tabTypes = ["Payload Types", "C2 Profiles", "Translators", "Command Augmentation", "3rd Party", "Webhooks", "Loggers", "Eventing", "Auth", ];
|
||||
const tabTypes = ["Payload Types", "C2 Profiles", "Translators", "Command Augmentation", "3rd Party", "Webhooks", "Loggers", "Eventing", "Auth", "Browsers"];
|
||||
|
||||
const filterDeleted = (c, showDeleted) => {
|
||||
if(showDeleted){
|
||||
@@ -144,12 +167,16 @@ export function PayloadTypesC2Profiles({me}){
|
||||
customSubscription: sub_consuming_services,
|
||||
dataKey: "consuming_container"
|
||||
});
|
||||
const customBrowsers = useCustomSubscription({
|
||||
customSubscription: SUB_Custom_Browsers,
|
||||
dataKey: "custombrowser"
|
||||
})
|
||||
const [showDeleted, setShowDeleted] = React.useState(false);
|
||||
React.useEffect( () => {
|
||||
let newData = [...payloadTypes, ...c2Profiles, ...translationContainers, ...consumingServices];
|
||||
let newData = [...payloadTypes, ...c2Profiles, ...translationContainers, ...consumingServices, ...customBrowsers];
|
||||
newData.sort( (a, b) => a.name.toLowerCase() < b.name.toLowerCase() ? -1 : a.name.toLowerCase() > b.name.toLowerCase() ? 1 : 0);
|
||||
setAllData(newData);
|
||||
}, [payloadTypes, c2Profiles, translationContainers, consumingServices])
|
||||
}, [payloadTypes, c2Profiles, translationContainers, consumingServices, customBrowsers])
|
||||
const handleChange = (event, newValue) => {
|
||||
setValue(newValue);
|
||||
};
|
||||
@@ -182,16 +209,16 @@ export function PayloadTypesC2Profiles({me}){
|
||||
case 8:
|
||||
return <ContainersTabConsumingServicesPanel key={"Auth"} type={"Auth"} index={value} value={value} showDeleted={showDeleted}
|
||||
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "consuming_container" && c.type === "auth")} />
|
||||
case 9:
|
||||
return <ContainersTabCustomBrowsersPanel key={"Browsers"} type={"Browsers"} index={value} value={value} showDeleted={showDeleted}
|
||||
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "custombrowser")} />
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div style={{ height: "100%", display: "flex", flexDirection: "column", width: "100%"}}>
|
||||
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main}} variant={"elevation"}>
|
||||
<Typography variant="h5" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
|
||||
Installed Services
|
||||
</Typography>
|
||||
<MythicPageBody>
|
||||
<MythicPageHeader title={"Installed Services"}>
|
||||
{showDeleted ? (
|
||||
<MythicStyledTooltip title={"Hide Deleted Services"} tooltipStyle={{float: "right"}}>
|
||||
<IconButton size="small" style={{float: "right"}} variant="contained" onClick={() => setShowDeleted(!showDeleted)}><VisibilityIcon /></IconButton>
|
||||
@@ -202,7 +229,7 @@ export function PayloadTypesC2Profiles({me}){
|
||||
<IconButton size="small" style={{float: "right"}} variant="contained" onClick={() => setShowDeleted(!showDeleted)} ><VisibilityOffIcon /></IconButton>
|
||||
</MythicStyledTooltip>
|
||||
)}
|
||||
</Paper>
|
||||
</MythicPageHeader>
|
||||
<AppBar position="static" color="default" className={"no-box-shadow"}>
|
||||
<Tabs
|
||||
value={value}
|
||||
@@ -243,6 +270,10 @@ export function PayloadTypesC2Profiles({me}){
|
||||
case "Auth":
|
||||
return <ContainersTabAuthLabel key={"auth"}
|
||||
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "consuming_container" && c.type === "auth")}/>;
|
||||
case "Browsers":
|
||||
return <ContainersTabBrowsersLabel key={"browsers"}
|
||||
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "custombrowser")}/>;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -253,7 +284,7 @@ export function PayloadTypesC2Profiles({me}){
|
||||
{
|
||||
getTabComponent()
|
||||
}
|
||||
</div>
|
||||
</MythicPageBody>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -270,21 +301,21 @@ const PayloadTypesC2ProfilesTableRow = ({service, showDeleted}) => {
|
||||
|
||||
const ContainersTabPayloadTypesLabel = (props) => {
|
||||
return (
|
||||
<MythicSearchTabLabel label={"Payload Types" + (props.containers.length > 0 ? " (" + props.containers.length + ") " : "")} iconComponent={
|
||||
<MythicSearchTabLabel label={"Agents" + (props.containers.length > 0 ? " (" + props.containers.length + ") " : "")} iconComponent={
|
||||
<></>
|
||||
} {...props}/>
|
||||
)
|
||||
}
|
||||
const ContainersTab3rdPartyLabel = (props) => {
|
||||
return (
|
||||
<MythicSearchTabLabel label={"3rd Party Service" + (props.containers.length > 0 ? " (" + props.containers.length + ") " : "")} iconComponent={
|
||||
<MythicSearchTabLabel label={"External" + (props.containers.length > 0 ? " (" + props.containers.length + ") " : "")} iconComponent={
|
||||
<></>
|
||||
} {...props}/>
|
||||
)
|
||||
}
|
||||
const ContainersTabC2ProfilesLabel = (props) => {
|
||||
return (
|
||||
<MythicSearchTabLabel label={"C2 Profiles" + (props.containers.length > 0 ? " (" + props.containers.length + ") " : "")} iconComponent={
|
||||
<MythicSearchTabLabel label={"C2" + (props.containers.length > 0 ? " (" + props.containers.length + ") " : "")} iconComponent={
|
||||
<></>
|
||||
} {...props}/>
|
||||
)
|
||||
@@ -326,7 +357,14 @@ const ContainersTabAuthLabel = (props) => {
|
||||
}
|
||||
const ContainersTabCommandAugmentLabel = (props) => {
|
||||
return (
|
||||
<MythicSearchTabLabel label={"Command Augmentation" + (props.containers.length > 0 ? " (" + props.containers.length + ") " : "")} iconComponent={
|
||||
<MythicSearchTabLabel label={"Command Augments" + (props.containers.length > 0 ? " (" + props.containers.length + ") " : "")} iconComponent={
|
||||
<></>
|
||||
} {...props}/>
|
||||
)
|
||||
}
|
||||
const ContainersTabBrowsersLabel = (props) => {
|
||||
return (
|
||||
<MythicSearchTabLabel label={"Browsers" + (props.containers.length > 0 ? " (" + props.containers.length + ") " : "")} iconComponent={
|
||||
<></>
|
||||
} {...props}/>
|
||||
)
|
||||
@@ -361,6 +399,9 @@ Command Augmentation container commands get "injected" into the callbacks of sup
|
||||
These are containers that generate "callbacks" and allow you to interact with a 3rd Party Service over an API from Mythic's standard tasking workflow.
|
||||
A few examples are github.com/MythicAgents/bloodhound and github.com/MythicAgents/ghostwriter.`;
|
||||
break;
|
||||
case "Browsers":
|
||||
message = `Custom Browsers allow you to collect and display data in a familiar and unified "browser" format.`;
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
@@ -473,3 +514,57 @@ You can extend this auth capability to support your own LDAP, SSO, or otherwise
|
||||
</MythicTabPanel>
|
||||
)
|
||||
}
|
||||
const ContainersTabCustomBrowsersPanel = (props) => {
|
||||
const getEmptyServiceListMessage = () => {
|
||||
let message = "";
|
||||
switch(props.type){
|
||||
case "Browsers":
|
||||
message = `Custom Browsers allow you to collect and display data in a familiar and unified "browser" format.`;
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid black",
|
||||
padding: "5px",
|
||||
backgroundColor: "rgba(37,37,37,0.92)", color: "white",
|
||||
whiteSpace: "pre-wrap"
|
||||
}}>
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<MythicTabPanel {...props} >
|
||||
<div style={{display: "flex", flexGrow: 1, overflowY: "auto"}}>
|
||||
<TableContainer>
|
||||
<Table stickyHeader size="small" style={{"maxWidth": "100%", "overflow": "scroll"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicTableCell style={{width: "4rem"}}>Delete</MythicTableCell>
|
||||
<MythicTableCell style={{width: "90px"}}></MythicTableCell>
|
||||
<MythicTableCell>Service</MythicTableCell>
|
||||
<MythicTableCell style={{width: "4rem"}}>Type</MythicTableCell>
|
||||
<MythicTableCell>Metadata</MythicTableCell>
|
||||
<MythicTableCell>Manage</MythicTableCell>
|
||||
<MythicTableCell></MythicTableCell>
|
||||
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{props.containers.map((service, index) => (
|
||||
<CustomBrowserRow key={"service" + index} service={service} showDeleted={props.showDeleted}/>
|
||||
))}
|
||||
{props.containers.length === 0 && getEmptyServiceListMessage()}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</MythicTabPanel>
|
||||
)
|
||||
}
|
||||
@@ -17,17 +17,6 @@ import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import {MythicDialog} from "../../MythicComponents/MythicDialog";
|
||||
import {C2ProfileListFilesDialog} from "./C2ProfileListFilesDialog";
|
||||
|
||||
const PREFIX = 'TranslationContainerCard';
|
||||
|
||||
const classes = {
|
||||
root: `${PREFIX}-root`,
|
||||
expand: `${PREFIX}-expand`,
|
||||
expandOpen: `${PREFIX}-expandOpen`,
|
||||
running: `${PREFIX}-running`,
|
||||
notrunning: `${PREFIX}-notrunning`
|
||||
};
|
||||
|
||||
|
||||
const toggleDeleteStatus = gql`
|
||||
mutation toggleC2ProfileDeleteStatus($translationcontainer_id: Int!, $deleted: Boolean!){
|
||||
update_translationcontainer_by_pk(pk_columns: {id: $translationcontainer_id}, _set: {deleted: $deleted}) {
|
||||
|
||||
@@ -100,6 +100,7 @@ query GetPayloadDetails($payload_id: Int!) {
|
||||
buildparameter {
|
||||
description
|
||||
parameter_type
|
||||
name
|
||||
}
|
||||
}
|
||||
os
|
||||
@@ -109,6 +110,7 @@ query GetPayloadDetails($payload_id: Int!) {
|
||||
c2profileparameter {
|
||||
description
|
||||
parameter_type
|
||||
name
|
||||
}
|
||||
c2profile {
|
||||
name
|
||||
@@ -411,7 +413,7 @@ function DetailedPayloadInnerTable(props){
|
||||
setCommands(commandState);
|
||||
const buildParametersState = data.payload[0].buildparameterinstances.map( (b) =>
|
||||
{
|
||||
return {description: b.buildparameter.description,
|
||||
return {description: b.buildparameter.description, name: b.buildparameter.name,
|
||||
value: b.value,
|
||||
parameter_type: b.buildparameter.parameter_type,
|
||||
enc_key: b.enc_key_base64,
|
||||
@@ -424,6 +426,7 @@ function DetailedPayloadInnerTable(props){
|
||||
if( !(headerName in prev) ){
|
||||
return {...prev, [headerName]:
|
||||
[{description: cur.c2profileparameter.description,
|
||||
name: cur.c2profileparameter.name,
|
||||
value: cur.value,
|
||||
count: cur.count,
|
||||
enc_key: cur.enc_key_base64,
|
||||
@@ -434,6 +437,7 @@ function DetailedPayloadInnerTable(props){
|
||||
}
|
||||
return {...prev, [headerName]: [...prev[headerName],
|
||||
{description: cur.c2profileparameter.description,
|
||||
name: cur.c2profileparameter.name,
|
||||
value: cur.value,
|
||||
count: cur.count,
|
||||
enc_key: cur.enc_key_base64,
|
||||
@@ -445,7 +449,7 @@ function DetailedPayloadInnerTable(props){
|
||||
const c2ProfilesState = Object.keys(c2Profiles).reduce( (prev, cur) => {
|
||||
return [...prev, {
|
||||
c2_profile: cur,
|
||||
parameters: c2Profiles[cur].sort((a,b) => (a.description > b.description) ? 1: ((b.description > a.description) ? -1 : 0))
|
||||
parameters: c2Profiles[cur].sort((a,b) => (a.name > b.name) ? 1: ((b.name > a.name) ? -1 : 0))
|
||||
}];
|
||||
}, []);
|
||||
setC2Profiles(c2ProfilesState);
|
||||
@@ -598,6 +602,10 @@ function DetailedPayloadInnerTable(props){
|
||||
}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>Agent File ID</TableCell>
|
||||
<TableCell>{data.payload[0].filemetum.agent_file_id}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>SHA1</TableCell>
|
||||
<TableCell>{data.payload[0].filemetum.sha1}</TableCell>
|
||||
@@ -664,7 +672,14 @@ function DetailedPayloadInnerTable(props){
|
||||
{
|
||||
buildParameters.map( (cmd, i) => (
|
||||
<TableRow key={"buildprop" + i + "for" + props.payload_id} hover>
|
||||
<TableCell>{cmd.description}</TableCell>
|
||||
<TableCell>
|
||||
<Typography style={{fontWeight: "600"}} >
|
||||
{cmd.name}
|
||||
</Typography>
|
||||
<Typography style={{fontSize: theme.typography.pxToRem(15), marginLeft: "10px"}}>
|
||||
{cmd.description}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ParseForDisplay cmd={cmd} />
|
||||
{cmd.enc_key === null ? null : (<React.Fragment>
|
||||
@@ -726,7 +741,14 @@ function DetailedPayloadInnerTable(props){
|
||||
{
|
||||
c2.parameters.map( (cmd, j) => (
|
||||
<TableRow key={"c2frag" + props.payload_id + c2.c2_profile + j} hover>
|
||||
<TableCell>{cmd.description}</TableCell>
|
||||
<TableCell>
|
||||
<Typography style={{fontWeight: "600"}} >
|
||||
{cmd.name}
|
||||
</Typography>
|
||||
<Typography style={{fontSize: theme.typography.pxToRem(15), marginLeft: "10px"}}>
|
||||
{cmd.description}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ParseForDisplay cmd={cmd} />
|
||||
{cmd.enc_key === null ? null : (<React.Fragment>
|
||||
|
||||
@@ -52,17 +52,14 @@ export function EditPayloadConfigDialog(props) {
|
||||
createPayloadMutation({variables: {payload: updatedConfig}}).catch( (e) => {console.log(e)} );
|
||||
props.onClose();
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
return (
|
||||
<MythicModifyStringDialog title={"Create New Payload With Modified Config"}
|
||||
value={payloadConfig}
|
||||
dontCloseOnSubmit={true}
|
||||
onSubmit={onCommitSubmit}
|
||||
onSubmitText={"Create"}
|
||||
onClose={props.onClose}
|
||||
maxRows={60}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -110,16 +107,13 @@ export function ViewPayloadConfigJSON(props) {
|
||||
}, [props.value]);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div style={{display: "flex", }}>
|
||||
<div style={{display: "flex", height: "100%" }}>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
|
||||
width="100%"
|
||||
height={"100%"}
|
||||
showPrintMargin={false}
|
||||
wrapEnabled={true}
|
||||
minLines={10}
|
||||
maxLines={50}
|
||||
value={payloadConfig}
|
||||
focus={true}
|
||||
onChange={onChange}
|
||||
|
||||
@@ -5,6 +5,7 @@ import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import {gql, useMutation} from '@apollo/client';
|
||||
import { snackActions } from '../../utilities/Snackbar';
|
||||
import {DragAndDropFileUpload} from "../Callbacks/TaskParametersDialogRow";
|
||||
|
||||
const create_payload = gql`
|
||||
mutation createPayloadMutation($payload: String!) {
|
||||
@@ -31,22 +32,19 @@ export function ImportPayloadConfigDialog(props) {
|
||||
createPayloadMutation({variables: {payload: fileValue.contents}}).catch( (e) => {console.log(e)} );
|
||||
props.onClose();
|
||||
}
|
||||
const onFileChange = (evt) => {
|
||||
const onFileChange = (newFile) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const contents = e.target.result;
|
||||
setFileValue({name: evt.target.files[0].name, contents: contents});
|
||||
setFileValue({name: newFile.name, contents: contents});
|
||||
}
|
||||
reader.readAsBinaryString(evt.target.files[0]);
|
||||
reader.readAsBinaryString(newFile);
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Import Payload Config to Generate New Payload</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
<Button variant="contained" component="label">
|
||||
{ fileValue.name === "" ? "Select File" : fileValue.name }
|
||||
<input onChange={onFileChange} type="file" hidden />
|
||||
</Button>
|
||||
<DragAndDropFileUpload value={fileValue} multiple={false} onChange={onFileChange} />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" onClick={props.onClose} color="primary">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { PayloadsTable } from './PayloadsTable';
|
||||
import {useMutation, gql, useSubscription, useQuery, useLazyQuery} from '@apollo/client';
|
||||
import { snackActions } from '../../utilities/Snackbar';
|
||||
import {getSkewedNow} from "../../utilities/Time";
|
||||
import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
|
||||
|
||||
export const payloadFragment = gql`
|
||||
fragment payloadData on payload {
|
||||
@@ -16,9 +17,11 @@ fragment payloadData on payload {
|
||||
description
|
||||
deleted
|
||||
auto_generated
|
||||
payload_type_semver
|
||||
payloadtype {
|
||||
id
|
||||
name
|
||||
semver
|
||||
}
|
||||
payload_build_steps(order_by: {step_number: asc}) {
|
||||
step_name
|
||||
@@ -323,7 +326,7 @@ export function Payloads(props){
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
return (
|
||||
<div style={{display: "flex", height: "100%", flexDirection: "column", position: "relative"}}>
|
||||
<MythicPageBody>
|
||||
|
||||
<PayloadsTable pageData={pageData} me={props.me}
|
||||
openBackdrop={openBackdrop}
|
||||
@@ -336,7 +339,7 @@ export function Payloads(props){
|
||||
onCallbacksAllowedChanged={onCallbacksAllowedChanged}
|
||||
onChangeShowAutogenerated={onChangeShowAutogenerated}
|
||||
/>
|
||||
</div>
|
||||
</MythicPageBody>
|
||||
|
||||
);
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import {useNavigate} from 'react-router-dom';
|
||||
import Pagination from '@mui/material/Pagination';
|
||||
import { Backdrop, CircularProgress } from '@mui/material';
|
||||
import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
|
||||
|
||||
|
||||
export function PayloadsTable({payload, onDeletePayload, onUpdateCallbackAlert, onRestorePayload, me,
|
||||
@@ -74,70 +75,68 @@ export function PayloadsTable({payload, onDeletePayload, onUpdateCallbackAlert,
|
||||
setDropdownOpen(false);
|
||||
};
|
||||
return (
|
||||
<div style={{display: "flex", flexDirection: "column", height: "100%"}}>
|
||||
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main}} variant={"elevation"}>
|
||||
<Typography variant="h5" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
|
||||
Payloads
|
||||
</Typography>
|
||||
<>
|
||||
<MythicPageHeader title={"Payloads"}>
|
||||
|
||||
<ButtonGroup variant="text" ref={dropdownAnchorRef} aria-label="split button" style={{float: "right", marginRight: "10px", color: "white"}}>
|
||||
<Button size="small" style={{color: "white"}} aria-controls={dropdownOpen ? 'split-button-menu' : undefined}
|
||||
aria-expanded={dropdownOpen ? 'true' : undefined}
|
||||
aria-haspopup="menu"
|
||||
onClick={() => setDropdownOpen(!dropdownOpen)}>
|
||||
Actions <ArrowDropDownIcon />
|
||||
aria-expanded={dropdownOpen ? 'true' : undefined}
|
||||
aria-haspopup="menu"
|
||||
onClick={() => setDropdownOpen(!dropdownOpen)}>
|
||||
Actions <ArrowDropDownIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Popper open={dropdownOpen} anchorEl={dropdownAnchorRef.current} role={undefined} transition disablePortal style={{zIndex: 10}}>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
|
||||
}}
|
||||
>
|
||||
<Paper className={"dropdownMenuColored"}>
|
||||
<ClickAwayListener onClickAway={() => setDropdownOpen(false)}>
|
||||
<MenuList id="split-button-menu">
|
||||
{dropDownOptions.map((option, index) => (
|
||||
<MenuItem
|
||||
key={option.name}
|
||||
onClick={(event) => handleMenuItemClick(event, index)}
|
||||
>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
|
||||
}}
|
||||
>
|
||||
<Paper className={"dropdownMenuColored"}>
|
||||
<ClickAwayListener onClickAway={() => setDropdownOpen(false)}>
|
||||
<MenuList id="split-button-menu">
|
||||
{dropDownOptions.map((option, index) => (
|
||||
<MenuItem
|
||||
key={option.name}
|
||||
onClick={(event) => handleMenuItemClick(event, index)}
|
||||
>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
{openPayloadImport &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openPayloadImport}
|
||||
onClose={()=>{setOpenPayloadImport(false);}}
|
||||
innerDialog={<ImportPayloadConfigDialog onClose={()=>{setOpenPayloadImport(false);}} />}
|
||||
onClose={()=>{setOpenPayloadImport(false);}}
|
||||
innerDialog={<ImportPayloadConfigDialog onClose={()=>{setOpenPayloadImport(false);}} />}
|
||||
/>
|
||||
}
|
||||
</Paper>
|
||||
</MythicPageHeader>
|
||||
<div style={{flexGrow: 1, overflowY: "auto", height: "100%", position: "relative"}}>
|
||||
{openBackdrop &&
|
||||
<Backdrop open={openBackdrop} style={{zIndex: 2000, position: "absolute"}}>
|
||||
<CircularProgress color="inherit" disableShrink />
|
||||
</Backdrop>
|
||||
}
|
||||
<Table stickyHeader size="small" style={{ "maxWidth": "100%", "overflow": "auto", tableLayout: "fixed"}}>
|
||||
<TableHead >
|
||||
<TableRow>
|
||||
<TableCell style={{width: "6rem"}}></TableCell>
|
||||
<TableCell style={{width: "3rem"}}></TableCell>
|
||||
<TableCell>File</TableCell>
|
||||
<TableCell>Progress</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
<TableCell >C2 Status</TableCell>
|
||||
<TableCell>Tags</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<Table stickyHeader size="small" style={{ "maxWidth": "100%", "overflow": "auto", tableLayout: "fixed"}}>
|
||||
<TableHead >
|
||||
<TableRow>
|
||||
<TableCell style={{width: "6rem"}}></TableCell>
|
||||
<TableCell style={{width: "3rem"}}></TableCell>
|
||||
<TableCell>File</TableCell>
|
||||
<TableCell>Progress</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
<TableCell >C2 Status</TableCell>
|
||||
<TableCell>Tags</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{payload.map( (op) => (
|
||||
<PayloadsTableRow
|
||||
me={me}
|
||||
@@ -150,15 +149,16 @@ export function PayloadsTable({payload, onDeletePayload, onUpdateCallbackAlert,
|
||||
{...op}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div style={{background: "transparent", display: "flex", justifyContent: "center", alignItems: "center", paddingTop: "5px", paddingBottom: "10px"}}>
|
||||
<Pagination count={Math.ceil(pageData.totalCount / pageData.fetchLimit)} variant="outlined" color="primary" boundaryCount={1}
|
||||
siblingCount={1} onChange={onChangePage} showFirstButton={true} showLastButton={true} style={{padding: "20px"}}/>
|
||||
siblingCount={1} onChange={onChangePage} showFirstButton={true} showLastButton={true} style={{padding: "20px"}}/>
|
||||
<Typography style={{paddingLeft: "10px"}}>Total Results: {pageData.totalCount}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,12 +5,7 @@ import IconButton from '@mui/material/IconButton';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import { MythicDialog } from '../../MythicComponents/MythicDialog';
|
||||
import {DetailedPayloadComparisonTable, DetailedPayloadTable} from './DetailedPayloadTable';
|
||||
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';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {MythicConfirmDialog} from '../../MythicComponents/MythicConfirmDialog';
|
||||
import {PayloadDescriptionDialog} from './PayloadDescriptionDialog';
|
||||
import {PayloadFilenameDialog} from './PayloadFilenameDialog';
|
||||
@@ -44,13 +39,14 @@ import {PayloadGetIOCDialog} from "./PayloadGetIOCDialog";
|
||||
import {PayloadGetSampleMessageDialog} from "./PayloadGetSampleMessageDialog";
|
||||
import IosShareIcon from '@mui/icons-material/IosShare';
|
||||
import {TagsDisplay, ViewEditTags} from "../../MythicComponents/MythicTag";
|
||||
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
|
||||
import {MythicAgentSVGIcon, MythicAgentSVGIconNoTooltip} from "../../MythicComponents/MythicAgentSVGIcon";
|
||||
import SmartToyTwoToneIcon from '@mui/icons-material/SmartToyTwoTone';
|
||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||
import BlockIcon from '@mui/icons-material/Block';
|
||||
import NotificationsOffOutlinedIcon from '@mui/icons-material/NotificationsOffOutlined';
|
||||
import {EditPayloadConfigDialog} from "./EditPayloadConfigDialog";
|
||||
import DifferenceIcon from '@mui/icons-material/Difference';
|
||||
import {Dropdown, DropdownMenuItem, DropdownNestedMenuItem} from "../../MythicComponents/MythicNestedMenus";
|
||||
|
||||
const rebuildPayloadMutation = gql`
|
||||
mutation triggerRebuildMutation($uuid: String!) {
|
||||
@@ -146,90 +142,195 @@ export function PayloadsTableRow(props){
|
||||
const onCallbacksAllowedChanged = () =>[
|
||||
props.onCallbacksAllowedChanged(props.uuid, !props.callback_allowed)
|
||||
]
|
||||
const handleMenuItemClick = (event, index) => {
|
||||
options[index].click();
|
||||
const handleMenuItemClick = (event, clickOption) => {
|
||||
clickOption({event});
|
||||
setOpenUpdateDialog(false);
|
||||
};
|
||||
const options = [
|
||||
{name: <><DriveFileRenameOutlineIcon style={{marginRight: "10px"}}/> {"Rename File"}</> , click: () => {
|
||||
setOpenFilenameDialog(true);
|
||||
}},
|
||||
{name: <><DescriptionIcon color={"info"} style={{marginRight: "10px"}} />{'Edit Description'}</>, click: () => {
|
||||
setOpenDescriptionDialog(true);
|
||||
}},
|
||||
{name: <><InfoIconOutline color={"info"} style={{marginRight: "10px"}} /> View Payload Configuration</>, click: () => {
|
||||
setOpenDetailedView(true);
|
||||
}},
|
||||
{name: <><DifferenceIcon color={"info"} style={{marginRight: "10px"}} /> Compare Payload Configuration</>, click: () => {
|
||||
setOpenComparePayloadsDialog(true);
|
||||
}},
|
||||
{name: props.callback_alert ?
|
||||
<><VisibilityIcon color={"success"} style={{marginRight: "10px"}} />{'Alerting to New Callbacks'}</> :
|
||||
<><VisibilityOffIcon color={"error"} style={{marginRight: "10px"}} />{"Not Alerting to New Callbacks"}</>,
|
||||
click: () => {
|
||||
onAlertChanged();
|
||||
}},
|
||||
{name: props.callback_allowed ?
|
||||
<><VisibilityIcon color={"success"} style={{marginRight: "10px"}} />{'Allowing New Callbacks from this Payload'}</> :
|
||||
<><VisibilityOffIcon color={"error"} style={{marginRight: "10px"}} />{"Preventing New Callbacks from this Payload"}</>,
|
||||
click: () => {
|
||||
onCallbacksAllowedChanged();
|
||||
}},
|
||||
{name: <><MessageIcon style={{marginRight: "10px"}} />{'View Build Message/Stdout'} </> , click: () => {
|
||||
setViewError(false);
|
||||
setOpenBuildMessageDialog(true);
|
||||
}},
|
||||
{name: <><ErrorIcon color={"error"} style={{marginRight: "10px"}} />{'View Build Errors'}</>, click: () => {
|
||||
setViewError(true);
|
||||
setOpenBuildMessageDialog(true);
|
||||
}},
|
||||
{name: <><CachedIcon color={"success"} style={{marginRight: "10px"}} />{'Trigger New Build'}</>, click: () => {
|
||||
triggerRebuild({variables: {uuid: props.uuid}});
|
||||
}},
|
||||
{name: <><CachedIcon color={"success"} style={{marginRight: "10px"}} />{'Trigger New Build With Edits'}</>, click: () => {
|
||||
setOpenEditPayloadConfigDialog(true);
|
||||
}},
|
||||
{name: <><IosShareIcon color={"info"} style={{marginRight: "10px"}} />{'Export Payload Config'}</>, click: () => {
|
||||
exportConfig({variables: {uuid: props.uuid}});
|
||||
}},
|
||||
{name: <><PhoneMissedIcon style={{marginRight: "10px"}} />{'Generate Redirect Rules'}</>, click: () => {
|
||||
setOpenRedirectRulesDialog(true);
|
||||
}},
|
||||
{name: <><VerifiedIcon style={{marginRight: "10px"}} />{'Check Agent C2 Configuration'}</>, click: () => {
|
||||
setOpenConfigCheckDialog(true);
|
||||
}},
|
||||
{name: <><FingerprintIcon style={{marginRight: "10px"}} />{'Generate IOCs'}</>, click: () => {
|
||||
setOpenGenerateIOCDialog(true);
|
||||
}},
|
||||
{name: <><BiotechIcon style={{marginRight: "10px"}} />{'Generate Sample Message'}</>, click: () => {
|
||||
setOpenGenerateSampleMessageDialog(true);
|
||||
}},
|
||||
{name: <><AddIcCallIcon color={"success"} style={{marginRight: "10px"}} />{'Generate Fake Callback'}</>, click: () => {
|
||||
setOpenCreateNewCallbackDialog(true);
|
||||
}},
|
||||
{name:<>
|
||||
{props.deleted ? (
|
||||
<>
|
||||
<RestoreFromTrashIcon color={"success"} style={{marginRight: "10px"}} />
|
||||
Restore Payload
|
||||
</>
|
||||
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<DeleteIcon color={"error"} style={{marginRight: "10px"}}/>
|
||||
Delete the payload from disk
|
||||
</React.Fragment>
|
||||
)}</>, click: () => {
|
||||
setOpenDeleteDialog(true);
|
||||
}}
|
||||
]
|
||||
;
|
||||
{
|
||||
name: "Rename File", type: "item",
|
||||
icon: <DriveFileRenameOutlineIcon style={{marginRight: "10px"}}/>,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenFilenameDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Edit Description", type: "item",
|
||||
icon: <DescriptionIcon color={"info"} style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenDescriptionDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "View Payload Configuration", type: "item",
|
||||
icon: <InfoIconOutline color={"info"} style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenDetailedView(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Compare Payload Configuration", type: "item",
|
||||
icon: <DifferenceIcon color={"info"} style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenComparePayloadsDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: props.callback_alert ? "Alerting to New Callbacks" : "Not Alerting to New Callbacks",
|
||||
type: "item",
|
||||
icon: props.callback_alert ?
|
||||
<VisibilityIcon color={"success"} style={{marginRight: "10px"}} />:
|
||||
<VisibilityOffIcon color={"error"} style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onAlertChanged();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: props.callback_allowed ? "Allowing New Callbacks from this Payload" : "Preventing New Callbacks from this Payload",
|
||||
type: "item",
|
||||
icon: props.callback_allowed ?
|
||||
<VisibilityIcon color={"success"} style={{marginRight: "10px"}} /> :
|
||||
<VisibilityOffIcon color={"error"} style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onCallbacksAllowedChanged();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "View Build Message/Stdout",
|
||||
type: "item",
|
||||
icon: <MessageIcon style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setViewError(false);
|
||||
setOpenBuildMessageDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "View Build Errors",
|
||||
type: "item",
|
||||
icon: <ErrorIcon color={"error"} style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setViewError(true);
|
||||
setOpenBuildMessageDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Trigger New Build",
|
||||
type: "item",
|
||||
icon: <CachedIcon color={"success"} style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
triggerRebuild({variables: {uuid: props.uuid}});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Trigger New Build With Edits",
|
||||
type: "item",
|
||||
icon: <CachedIcon color={"success"} style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenEditPayloadConfigDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Export Payload Config",
|
||||
type: "item",
|
||||
icon: <IosShareIcon color={"info"} style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
exportConfig({variables: {uuid: props.uuid}});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Generate Redirect Rules",
|
||||
type: "item",
|
||||
icon: <PhoneMissedIcon style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenRedirectRulesDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Check Agent C2 Configuration",
|
||||
type: "item",
|
||||
icon: <VerifiedIcon style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenConfigCheckDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Generate IOCs",
|
||||
type: "item",
|
||||
icon: <FingerprintIcon style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenGenerateIOCDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Generate Sample Message",
|
||||
type: "item",
|
||||
icon: <BiotechIcon style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenGenerateSampleMessageDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Generate Fake Callback",
|
||||
type: "item",
|
||||
icon: <AddIcCallIcon color={"success"} style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenCreateNewCallbackDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: props.deleted ? "Restore Payload" : "Delete the Payload from Disk",
|
||||
type: "item",
|
||||
icon: props.deleted ? <RestoreFromTrashIcon color={"success"} style={{marginRight: "10px"}} /> :
|
||||
<DeleteIcon color={"error"} style={{marginRight: "10px"}}/>,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenDeleteDialog(true);
|
||||
}
|
||||
}
|
||||
];
|
||||
const handleClose = (event) => {
|
||||
if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
setOpenUpdateDialog(false);
|
||||
};
|
||||
};
|
||||
const openMenu = (event) => {
|
||||
dropdownAnchorRef.current = event?.currentTarget || event.target;
|
||||
setOpenUpdateDialog(true);
|
||||
}
|
||||
|
||||
const shouldDisplay = React.useMemo(() => {
|
||||
if(!props.deleted){
|
||||
@@ -245,35 +346,46 @@ export function PayloadsTableRow(props){
|
||||
<React.Fragment>
|
||||
<TableRow key={"payload" + props.uuid} hover>
|
||||
<MythicStyledTableCell>
|
||||
<Button ref={dropdownAnchorRef} size="small" onClick={()=>{setOpenUpdateDialog(true);}} >
|
||||
<Button size="small" onClick={openMenu} >
|
||||
Actions <ArrowDropDownIcon />
|
||||
</Button>
|
||||
{openUpdate &&
|
||||
<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" >
|
||||
{options.map((option, index) => (
|
||||
<MenuItem
|
||||
key={index + props.uuid}
|
||||
onClick={(event) => handleMenuItemClick(event, index)}
|
||||
>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
<Dropdown
|
||||
isOpen={dropdownAnchorRef.current}
|
||||
onOpen={setOpenUpdateDialog}
|
||||
externallyOpen={openUpdate}
|
||||
menu={
|
||||
options.map((option, index) => (
|
||||
option.type === 'item' ? (
|
||||
<DropdownMenuItem
|
||||
key={option.name}
|
||||
disabled={option.disabled}
|
||||
onClick={(event) => handleMenuItemClick(event, option.click)}
|
||||
>
|
||||
{option.icon}{option.name}
|
||||
</DropdownMenuItem>
|
||||
) : option.type === 'menu' ? (
|
||||
<DropdownNestedMenuItem
|
||||
label={option.name}
|
||||
disabled={option.disabled}
|
||||
menu={
|
||||
option.menuItems.map((menuOption, indx) => (
|
||||
<DropdownMenuItem
|
||||
key={menuOption.name}
|
||||
disabled={menuOption.disabled}
|
||||
onClick={(event) => handleMenuItemClick(event, menuOption.click)}
|
||||
>
|
||||
{menuOption.icon}{menuOption.name}
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
}
|
||||
/>
|
||||
) : null
|
||||
))
|
||||
}
|
||||
/>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
}
|
||||
{openDescription &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openDescription}
|
||||
@@ -340,8 +452,12 @@ export function PayloadsTableRow(props){
|
||||
}
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<MythicStyledTooltip title={props.payloadtype.name} tooltipStyle={{display: "inline-block"}}>
|
||||
<MythicAgentSVGIcon payload_type={props.payloadtype.name} style={{width: "35px", height: "35px"}} />
|
||||
<MythicStyledTooltip title={props.payloadtype.name +
|
||||
(props.payload_type_semver === "" ? "" : `\nBuilt w/ Version: v${props.payload_type_semver}`) +
|
||||
(props.payloadtype.semver === "" ? "" : `\nCurrent Version: v${props.payloadtype.semver}`)
|
||||
}
|
||||
tooltipStyle={{display: "inline-block", whiteSpace: "pre-wrap"}}>
|
||||
<MythicAgentSVGIconNoTooltip payload_type={props.payloadtype.name} style={{width: "35px", height: "35px"}} />
|
||||
</MythicStyledTooltip>
|
||||
{!props.callback_allowed &&
|
||||
<MythicStyledTooltip title={"No new Callbacks allowed from this payload"}
|
||||
|
||||
@@ -14,7 +14,7 @@ import TableRow from '@mui/material/TableRow';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
|
||||
export function PayloadsTableRowBuildProgress(props){
|
||||
const [buildProgressData, setBuildProgressData] = React.useState({
|
||||
@@ -109,23 +109,26 @@ export function PayloadsTableRowBuildProgress(props){
|
||||
}
|
||||
}
|
||||
return (
|
||||
<span style={props.build_phase === "success" ? {
|
||||
filter: "grayscale(1)",
|
||||
opacity: 0.5} : {}}>
|
||||
{buildProgressData.total_steps > 0 &&
|
||||
<>
|
||||
<span style={props.build_phase === "success" ? {
|
||||
filter: "grayscale(1)",
|
||||
opacity: 0.5} : {}}>
|
||||
{buildProgressData.total_steps > 0 &&
|
||||
props.payload_build_steps.map( step => (
|
||||
<MythicStyledTooltip title={step.step_name} key={"buildstep" + step.step_number}>
|
||||
{getButton(step)}
|
||||
</MythicStyledTooltip>
|
||||
))
|
||||
}
|
||||
{openStatusDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="lg" open={openStatusDialog}
|
||||
onClose={()=>{setOpenStatusDialog(false);}}
|
||||
innerDialog={<PayloadBuildStepStatusDialog step={displayData} onClose={()=>{setOpenStatusDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
</span>
|
||||
</span>
|
||||
{openStatusDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="lg" open={openStatusDialog}
|
||||
onClose={()=>{setOpenStatusDialog(false);}}
|
||||
innerDialog={<PayloadBuildStepStatusDialog step={displayData} onClose={()=>{setOpenStatusDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@@ -167,44 +170,43 @@ export function PayloadBuildStepStatusDialog(props) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Step {props.step.step_number + 1} - {props.step.step_name}</DialogTitle>
|
||||
<TableContainer className="mythicElement">
|
||||
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{width: "30%"}}>Parameter</TableCell>
|
||||
<TableCell >Value</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody style={{whiteSpace: "pre"}}>
|
||||
<TableRow hover>
|
||||
<TableCell>Step Start Time</TableCell>
|
||||
<TableCell>{props.step.start_time}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>Step End Time</TableCell>
|
||||
<TableCell>{props.step.end_time}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>Duration</TableCell>
|
||||
<TableCell>{duration}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell> {getStatusMessage()}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>Step Output</TableCell>
|
||||
<TableCell>{props.step.step_stdout}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>Step Error</TableCell>
|
||||
<TableCell>{props.step.step_stderr}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<DialogContent>
|
||||
<Table size="small" style={{ maxWidth: "100%", overflow: "scroll"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{width: "30%"}}>Parameter</TableCell>
|
||||
<TableCell >Value</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody style={{whiteSpace: "pre"}}>
|
||||
<TableRow hover>
|
||||
<TableCell>Step Start Time</TableCell>
|
||||
<TableCell>{props.step.start_time}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>Step End Time</TableCell>
|
||||
<TableCell>{props.step.end_time}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>Duration</TableCell>
|
||||
<TableCell>{duration}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell> {getStatusMessage()}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>Step Output</TableCell>
|
||||
<TableCell>{props.step.step_stdout}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>Step Error</TableCell>
|
||||
<TableCell>{props.step.step_stderr}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" onClick={props.onClose} color="primary">
|
||||
Close
|
||||
@@ -308,16 +310,11 @@ export function PayloadsTableRowBuildProcessPerStep(props){
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
{buildProgressData.total_steps > 0 ? (
|
||||
<React.Fragment>
|
||||
{props.payload_build_steps.map( step => (
|
||||
step.step_number === props.step_number ? (
|
||||
<React.Fragment key={"buildstep" + step.step_number}>{getButton(step)}</React.Fragment>
|
||||
) : null
|
||||
|
||||
))}
|
||||
</React.Fragment>
|
||||
) : null}
|
||||
{buildProgressData.total_steps > 0 &&
|
||||
props.payload_build_steps.map( step => (
|
||||
step.step_number === props.step_number &&
|
||||
<React.Fragment key={"buildstep" + step.step_number}>{getButton(step)}</React.Fragment>
|
||||
))}
|
||||
{openStatusDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="lg" open={openStatusDialog}
|
||||
onClose={()=>{setOpenStatusDialog(false);}}
|
||||
|
||||
Loaded 100 of 257 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user