Compare commits

..
1077 Commits
Author SHA1 Message Date
its-a-feature db91637b0a applying csv escape to database exports too 2026-11-06 12:52:29 -08:00
its-a-feature 7cf2fc886c escaping csv output 2026-11-06 12:45:44 -08:00
its-a-feature 950c9d31ea updating hasura read of invite link data 2026-11-05 20:23:46 -08:00
its-a-feature 9514f0c2f4 fix limiting operator/spectator query
scope interactive output more tightly by callback
2026-11-05 19:26:13 -08:00
its-a-feature e83a4c934e fixing authentication issues
* now able to revoke valid sessions/apitokens when logging out or when critical user fields change
* tighter checks when creating callbacks to make sure they're allowed
* explicit log out endpoint
2026-11-05 15:55:00 -08:00
its-a-feature 0bbd11018b addressing codex issues
sanitizing svg files, auth redirect locations, and further isolating browser script execution
2026-11-05 11:51:40 -08:00
its-a-feature 62c7b40c3f removed accidental early return 2026-11-05 08:03:48 -08:00
its-a-feature 9385312886 slight fix for momentjs pr 2026-11-04 16:51:32 -08:00
Cody Thomas add74090d3 Merge pull request #597 from thorinerr/remove-moment-dep
adding MythicTimestamp component to remove momentjs dependency
2026-11-04 18:45:13 -06:00
its-a-feature b31754519c cursor fixes to match PR #607 2026-11-04 16:17:32 -08:00
Cody Thomas 3e07d35639 Merge pull request #608 from s1nc3ju1y/fix-bot-apitoken-response-type
Fix error response type in GenerateAPITokenWebhook
2026-10-17 11:22:17 -05:00
s1nc3ju1y 4c98c7c0e9 Fix error response type in GenerateAPITokenWebhook 2026-10-17 17:21:43 +08:00
its-a-feature b25eb9c1ba fixing confusing container version error message 2026-10-11 13:16:56 -07:00
github-actions f57c60dbdb Bump Mythic Dockerfile tag to match release 'v4.0.0rc7' 2026-10-11 15:41:12 +00:00
its-a-feature aace617d87 v4 update for PR #594
Slight refactor of mythic-cli to support easier removal of services from mythic core. Removed env `jupyter_include` in favor of updating `jupyter_host=0.0.0.0` to match all other services
2026-10-11 08:23:39 -07:00
github-actions 9958208dfe Bump Mythic Dockerfile tag to match release 'v4.0.0rc6' 2026-10-08 16:57:09 +00:00
Cody Thomas 90efd23f38 Merge pull request #603 from Marmeus/patch-1
Update description in new trigger build
2026-10-08 10:59:11 -05:00
its-a-feature 3e7bb4883e v4 version of PR #589 to allow \t,\n,\r in pasted tasking 2026-10-08 08:41:08 -07:00
Marmeus e0698d4936 Update description in new trigger build 2026-10-01 17:43:18 +01:00
its-a-feature 6477dad87e added check to rsa.go for valid but wrong type 2026-09-27 13:23:05 -07:00
Cody Thomas 5b0591571c Merge pull request #596 from thorinerr/fix-pty-paste
fix copypaste into interactive terminal
2026-09-25 16:09:12 -05:00
its-a-feature fad1af30b3 update filter for operator/spectator to only see self 2026-09-20 19:57:54 -07:00
thorinerr 27af463511 adding serverSkew support to MythicTimestamp 2026-09-17 21:45:43 +03:00
thorinerr 26bf07e40c adding MythicTimestamp component to remove momentjs 2026-09-17 21:14:22 +03:00
thorinerr c84fbf98be fixing pasting in interactive terminal 2026-09-17 11:33:54 +03:00
its-a-feature f06b3ed2d1 refactored a bunch of UI pieces 2026-09-07 14:43:48 -07:00
its-a-feature 524cd0cf7e more server fixes and ui tweaks 2026-08-31 12:34:23 -07:00
its-a-feature 9515273776 server fixes and some ui refactoring 2026-08-30 12:12:04 -07:00
its-a-feature b9fc1a5985 fixing git clone 2026-08-28 19:54:35 -07:00
its-a-feature d898c74f73 Initial pass to fix AI Scanned Vulnerabilities
Starts addressing:
* #578
* #579
* #580
* #581
* #582
* #583
* #584
2026-08-21 16:02:52 -07:00
github-actions a9add7f1f6 Bump Mythic Dockerfile tag to match release 'v4.0.0rc5' 2026-08-05 20:56:55 +00:00
its-a-feature c6b10cd01e adding jupyter_include and some ui fixes 2026-08-05 13:17:57 -07:00
its-a-feature 8af9386594 Merge remote-tracking branch 'origin/Mythic-v4.0.0' into Mythic-v4.0.0 2026-08-05 10:52:47 -07:00
its-a-feature 6bc0a79cc5 updated some final docker image formats 2026-08-05 10:51:42 -07:00
its-a-feature 8154414836 updating docker install for debian 13 2026-08-05 10:51:42 -07:00
its-a-feature 9078551d57 added initial v4.0 readme 2026-08-05 10:51:42 -07:00
github-actions 92764064c9 Bump Mythic Dockerfile tag to match release 'v4.0.0rc4' 2026-08-05 10:51:42 -07:00
its-a-feature fb3d15ba8e ui tweaks, chunk/offset transfer, resume, ui file edits 2026-08-05 10:51:42 -07:00
its-a-feature 33b5fc92be updated operator syncing and chat message fetching 2026-08-05 10:51:42 -07:00
github-actions 0aebae8cdb Bump Mythic Dockerfile tag to match release 'v4.0.0rc3' 2026-08-05 10:51:42 -07:00
its-a-feature 5f0b080911 adjusting payloads table buttons 2026-08-05 10:51:41 -07:00
its-a-feature 3550ace673 fixed artifact search, more UI component standardization 2026-08-05 10:51:41 -07:00
github-actions ea53eafc1f Bump Mythic Dockerfile tag to match release 'v4.0.0rc2' 2026-08-05 10:51:41 -07:00
its-a-feature e1bc3f6d29 updating release branch 2026-08-05 10:51:21 -07:00
its-a-feature 724492b217 updating dockerfile 2026-08-05 10:51:21 -07:00
its-a-feature 84f5cdea4d updated build process for ui 2026-08-05 10:51:21 -07:00
its-a-feature 0a76e2a5d5 first v4.0 ui build with chunking 2026-08-05 10:51:21 -07:00
its-a-feature 9ea8c11931 updating github actions 2026-08-05 10:49:41 -07:00
its-a-feature 45e4e858d7 ui tweaks 2026-08-05 10:49:41 -07:00
its-a-feature 6ec1790267 UI refactor 2026-08-05 10:49:41 -07:00
its-a-feature b432c75858 updated task keyword reference expansion 2026-08-05 10:49:40 -07:00
its-a-feature 073edfb0b7 updated for build metadata and updated wrapping support 2026-08-05 10:49:40 -07:00
its-a-feature 7683d55761 bunch of ui performance enhancements 2026-08-05 10:49:40 -07:00
its-a-feature e0e9e7a388 updated header height on small width and chat streaming 2026-08-05 10:49:39 -07:00
its-a-feature 50f82239de fixed new channel select bug 2026-08-05 10:49:39 -07:00
its-a-feature 84da7a0207 removing border 2026-08-05 10:49:39 -07:00
its-a-feature 33428beed4 fixing chat icon scope 2026-08-05 10:49:39 -07:00
its-a-feature 5c60aef214 chat container icons, clickable meta chips, markdown subagent summary 2026-08-05 10:49:39 -07:00
its-a-feature 25f1892382 bump version 2026-08-05 10:49:39 -07:00
its-a-feature a014f24049 updated to allow post-cancel updates from chats 2026-08-05 10:49:39 -07:00
its-a-feature 35c5eba2ad updated icons, subagent flow, and end dates 2026-08-05 10:49:39 -07:00
its-a-feature 2b160c8d0b sub agents, customized metadata chips 2026-08-05 10:49:39 -07:00
its-a-feature aa6aa32220 chat history and timestamp update 2026-08-05 10:49:38 -07:00
its-a-feature 685ebf539f fixing slash command iteration over string vs struct 2026-08-05 10:49:38 -07:00
its-a-feature 43e5059f65 updated global setting check 2026-08-05 10:49:38 -07:00
its-a-feature faeece282c updating mcp to generic user input requests 2026-08-05 10:49:38 -07:00
its-a-feature 2a5dbd5aec added mute field to chat channels 2026-08-05 10:49:25 -07:00
its-a-feature c60852106e better background tracking 2026-08-05 10:49:25 -07:00
its-a-feature 291a9ec225 updated process browser format 2026-08-05 10:49:25 -07:00
its-a-feature 0afc6d11cf update mitre attack, fix upload tracking bug, add more file browser 2026-08-05 10:49:25 -07:00
its-a-feature 09c0830059 update eventing edit/duplicate to use wizard stepper 2026-08-05 10:49:25 -07:00
its-a-feature 75435638f9 eventing apitoken scopes and hasura preflight scope checks 2026-08-05 10:49:25 -07:00
its-a-feature 156492e0ae reworked operator alias style 2026-08-05 10:49:25 -07:00
its-a-feature 8f8272d48e uploaded file edit/save functionality 2026-08-05 10:49:24 -07:00
its-a-feature 81845f89ea refactored task references and adding @link support 2026-08-05 10:49:24 -07:00
its-a-feature 82d86d03d8 speed up RPC calls 2026-08-05 10:49:24 -07:00
its-a-feature 580edf7afb try to prevent duplicate file uploads 2026-08-05 10:49:24 -07:00
its-a-feature 8b3c1a38e8 adding keyword resolution and reverting to tasks 2026-08-05 10:49:24 -07:00
its-a-feature 7632e39bdd updated cred parsing, kerberos display, and refactored ticket parsers 2026-08-05 10:49:24 -07:00
its-a-feature 3a602a6c36 updated kerberos parsing and browser script table text color rendering 2026-08-05 10:49:24 -07:00
its-a-feature 3d964ed575 updating for credential expansion support 2026-08-05 10:49:23 -07:00
its-a-feature 74ec235945 updating credential search and tasking processing 2026-08-05 10:49:23 -07:00
its-a-feature 1b138ca314 credential and operator alias updates 2026-08-05 10:49:23 -07:00
its-a-feature bb19be6b79 updated ui colors 2026-08-05 10:49:23 -07:00
its-a-feature 25e6dc2d3c export/import operator aliases 2026-08-05 10:49:23 -07:00
its-a-feature 7f242e1c2b add aliases, update tasking for aliases, more relaxed parsing 2026-08-05 10:49:23 -07:00
its-a-feature 2433088b54 updated login page and json inline schema 2026-08-05 10:49:23 -07:00
its-a-feature 9d6246c5f2 initial iteration of UI for Jack's schema viewer 2026-08-05 10:49:22 -07:00
its-a-feature 5d0ef7491b display names, choices display names, complex choices support 2026-08-05 10:49:22 -07:00
its-a-feature ecc6b8e426 adding complex_choices, other_parameters, and c2 param dynamic queries 2026-08-05 10:49:22 -07:00
its-a-feature ae35bdf8dc updates for new parameter type 2026-08-05 10:49:22 -07:00
its-a-feature 7e159f2c9e build parameter groups and normalizing build/c2 parameter features 2026-08-05 10:49:22 -07:00
its-a-feature ffbdf8bd0d hosting files updates 2026-08-05 10:49:22 -07:00
its-a-feature cf2e553b0e mcp tool confirmation and scripting normalization 2026-08-05 10:49:22 -07:00
its-a-feature b307204305 better support chat cancel 2026-08-05 10:49:22 -07:00
its-a-feature 1f019ba17c make and invalidate bot apitokens 2026-08-05 10:49:21 -07:00
its-a-feature 427620d4a8 create and invalidate bot apitokens for chat 2026-08-05 10:49:21 -07:00
its-a-feature eee698a7a6 adding authContext to gRPC calls 2026-08-05 10:49:21 -07:00
its-a-feature f23ca2d594 better markdown/terminal view for task output 2026-08-05 10:49:21 -07:00
its-a-feature 135e0bff6b block list updates, user interact eventing prompts 2026-08-05 10:49:21 -07:00
its-a-feature c9cc40b80d eventing prompt user input/approval 2026-08-05 10:49:21 -07:00
its-a-feature 71f8e2052b theme updates and better chat config components 2026-08-05 10:49:21 -07:00
its-a-feature 5ee2a93812 initial chat feature 2026-08-05 10:49:21 -07:00
its-a-feature f6f147eb97 RabbitMQ Auth Context tracking requirements 2026-08-05 10:49:20 -07:00
its-a-feature f7f69ea8da adjusted reporting page, payloads table, 2026-08-05 10:49:20 -07:00
its-a-feature ed381b1399 updated file browser for container files 2026-08-05 10:49:20 -07:00
its-a-feature 58c0b7864f added .dockerignore 2026-08-05 10:49:20 -07:00
its-a-feature faab721355 better docs pages 2026-08-05 10:49:20 -07:00
its-a-feature 4e244934c8 fixing sql queries and container down resolver 2026-08-05 10:49:20 -07:00
its-a-feature f7d8f8c36c updated installed services table 2026-08-05 10:49:20 -07:00
its-a-feature a64bdb2ad3 remove minimap adjust resolution of event log warnings 2026-08-05 10:49:19 -07:00
its-a-feature 62fe662910 per response decode to not bulk bail on bad entries 2026-08-05 10:49:19 -07:00
its-a-feature 2077c8027b delegate proxy check performance increases 2026-08-05 10:49:19 -07:00
its-a-feature b782df6e41 c2 diagram updates 2026-08-05 10:49:19 -07:00
its-a-feature 2b89e20ffd c2 profile path modernization 2026-08-05 10:49:19 -07:00
its-a-feature c7f4f5e2e7 delegate check improvements and process table ui updates 2026-08-05 10:49:19 -07:00
its-a-feature 1610a12612 better json table displays and search table responses 2026-08-05 10:49:02 -07:00
its-a-feature e34ee878af added file browser insert/delete performance updates 2026-08-05 10:49:02 -07:00
its-a-feature 83099dd0c1 better task completion tracking, proxy tracking, and search page updates 2026-08-05 10:49:02 -07:00
its-a-feature a66e3be57f updated task response iteration and ui task cli updates 2026-08-05 10:49:02 -07:00
its-a-feature df93cc075b proxy traffic perf updates and ui parameter dialog modernization 2026-08-05 10:49:02 -07:00
its-a-feature b0c4559cea better rabbitmq handling and task filter ui updates 2026-08-05 10:49:01 -07:00
its-a-feature 8af7821321 updates to task and response database tracking 2026-08-05 10:49:01 -07:00
its-a-feature 3f8bc556ce updated eventing and installed services tables 2026-08-05 10:49:01 -07:00
its-a-feature 3218a7bcff more detailed color settings 2026-08-05 10:49:01 -07:00
its-a-feature 04499b715e tag updates 2026-08-05 10:49:01 -07:00
its-a-feature d8307ef5fd updated terminal interactions 2026-08-05 10:49:01 -07:00
its-a-feature ff2e22684c remove support for unneeded browserscript result types 2026-08-05 10:49:01 -07:00
its-a-feature 7db3cdab1e updated table filtering 2026-08-05 10:49:01 -07:00
its-a-feature 1d8389eaf8 fixing single task view 2026-08-05 10:48:28 -07:00
its-a-feature f7ce9ef5ef dashboard upates 2026-08-05 10:48:28 -07:00
its-a-feature 5ca7e141ba eventing updates 2026-08-05 10:48:27 -07:00
its-a-feature 20a478d324 c2 path and eventing wizard updates 2026-08-05 10:48:27 -07:00
its-a-feature c5dcd00f39 callback table row updates 2026-08-05 10:48:27 -07:00
its-a-feature c7f52776af fixed callback table 2026-08-05 10:48:26 -07:00
its-a-feature d60be637bd updating operator settings and virtualized tables 2026-08-05 10:48:26 -07:00
its-a-feature 8a25e8c9d3 response updates 2026-08-05 10:48:25 -07:00
its-a-feature 80836e42fe updated browser script table page 2026-08-05 10:48:25 -07:00
its-a-feature 75fbf79b63 apitoken create updates 2026-08-05 10:48:25 -07:00
its-a-feature 1f5f8bd29a updated single task view page 2026-08-05 10:48:25 -07:00
its-a-feature 2c8d12086b updated builder and eventing pages 2026-08-05 10:48:24 -07:00
its-a-feature dde4c68f91 updating some of the modal rendering conditionals 2026-08-05 10:48:24 -07:00
its-a-feature 03164bf307 major updates of the search, eventing, and payloads pages 2026-08-05 10:48:24 -07:00
its-a-feature 9e6b710dc3 updating gitignore 2026-08-05 10:48:24 -07:00
its-a-feature f8dc580612 updating datagrid 2026-08-05 10:48:24 -07:00
its-a-feature dd27da8afa "modernizing" of the UI components 2026-08-05 10:48:23 -07:00
its-a-feature e7cc1fca42 adding xterm for interactive tasking responses 2026-08-05 10:48:23 -07:00
its-a-feature 2ee5528123 better column dragging for virtualized tables 2026-08-05 10:48:23 -07:00
its-a-feature e9f4da5483 better scrolling for interactive tasks 2026-08-05 10:48:23 -07:00
its-a-feature 21bc6c0bb0 better handling of pagination and streaming responses 2026-08-05 10:48:23 -07:00
its-a-feature 778d75a911 updating hasura actions to all be camelCase 2026-08-05 10:48:23 -07:00
its-a-feature e856626eab updated scope tracking 2026-08-05 10:48:23 -07:00
its-a-feature 424c3bc0b6 additional scopes for apitokens and their checks 2026-08-05 10:48:23 -07:00
its-a-feature d3950bf00c grpc typo fix 2026-08-05 10:48:22 -07:00
its-a-feature 59ddcbfd39 prevent duplicate onStart calls for the same container 2026-08-05 10:48:22 -07:00
its-a-feature 876003657d updated lock synchronization 2026-08-05 10:48:22 -07:00
its-a-feature e9529d1286 logging updates and more operation scope checks added to routes 2026-08-05 10:48:22 -07:00
its-a-feature 7db80934c0 more api validation and logging updates 2026-08-05 10:48:22 -07:00
its-a-feature 06f85b7d16 updating apitokens to be different, removing /api/v1.4 prefix, scopes 2026-08-05 10:48:22 -07:00
its-a-feature 61b1275526 starting auth adjustments to remove cookies 2026-08-05 10:48:22 -07:00
its-a-feature 5d1c01b6d1 updated some final docker image formats 2026-08-05 10:47:59 -07:00
its-a-feature 7239a577e4 updating docker install for debian 13 2026-07-31 13:26:28 -07:00
its-a-feature 2cfc5b2f53 added initial v4.0 readme 2026-07-31 11:54:08 -07:00
github-actions 895db29800 Bump Mythic Dockerfile tag to match release 'v4.0.0rc4' 2026-07-30 21:37:33 +00:00
its-a-feature d955242e1c ui tweaks, chunk/offset transfer, resume, ui file edits 2026-07-30 14:17:19 -07:00
its-a-feature b04737205d updated operator syncing and chat message fetching 2026-07-21 15:48:26 -07:00
github-actions d5c1da842d Bump Mythic Dockerfile tag to match release 'v4.0.0rc3' 2026-07-17 20:11:05 +00:00
its-a-feature b160fa9875 adjusting payloads table buttons 2026-07-17 12:50:35 -07:00
its-a-feature a993faf4da fixed artifact search, more UI component standardization 2026-07-17 10:15:47 -07:00
its-a-feature e37e057a89 process browser fix 2026-07-16 13:02:35 -07:00
github-actions d4d2efc654 Bump Mythic Dockerfile tag to match release 'v4.0.0rc2' 2026-07-15 23:13:05 +00:00
its-a-feature 6bb0a119f0 updating release branch 2026-07-15 15:53:27 -07:00
its-a-feature 98d8163400 updating dockerfile 2026-07-15 15:09:19 -07:00
its-a-feature 4f8c7d2935 updated build process for ui 2026-07-15 15:07:50 -07:00
its-a-feature d5acf79e33 first v4.0 ui build with chunking 2026-07-15 14:24:41 -07:00
its-a-feature 836780df90 updating github actions 2026-07-15 13:32:56 -07:00
its-a-feature f7bbdec0ee ui tweaks 2026-07-15 13:06:28 -07:00
its-a-feature 03ed56c2c5 UI refactor 2026-07-15 09:54:01 -07:00
its-a-feature efbfb4fc33 updated task keyword reference expansion 2026-07-14 11:06:52 -07:00
its-a-feature 48bea58d21 updated for build metadata and updated wrapping support 2026-07-14 08:09:29 -07:00
its-a-feature ac5977bdab bunch of ui performance enhancements 2026-07-11 11:24:06 -07:00
its-a-feature ee8008b8ba updated header height on small width and chat streaming 2026-07-10 11:01:03 -07:00
its-a-feature d213d7dd66 fixed new channel select bug 2026-07-10 08:44:47 -07:00
its-a-feature b766e08e0e removing border 2026-07-10 08:29:56 -07:00
its-a-feature 16f4c76a07 fixing chat icon scope 2026-07-10 08:26:17 -07:00
its-a-feature d0f38e1fdb chat container icons, clickable meta chips, markdown subagent summary 2026-07-09 15:39:12 -07:00
its-a-feature f64ccca874 bump version 2026-07-09 13:11:52 -07:00
its-a-feature 4488cfac94 updated to allow post-cancel updates from chats 2026-07-09 13:11:28 -07:00
its-a-feature 796d4937ba updated icons, subagent flow, and end dates 2026-07-09 12:08:44 -07:00
its-a-feature 738c80b049 sub agents, customized metadata chips 2026-07-08 21:42:00 -07:00
its-a-feature 4e6af3a864 chat history and timestamp update 2026-07-08 15:14:30 -07:00
its-a-feature f04095a837 fixing slash command iteration over string vs struct 2026-07-08 14:56:02 -07:00
its-a-feature 9999e163ee updated global setting check 2026-07-08 13:30:19 -07:00
its-a-feature 33ec001541 updating mcp to generic user input requests 2026-07-08 11:37:17 -07:00
its-a-feature c943e0fc65 added mute field to chat channels 2026-07-07 16:06:40 -07:00
its-a-feature b52c332e9b better background tracking 2026-07-07 08:43:36 -07:00
its-a-feature 54cf000a4c updated process browser format 2026-07-06 18:09:53 -07:00
its-a-feature 541704f4c7 update mitre attack, fix upload tracking bug, add more file browser 2026-07-06 15:29:21 -07:00
its-a-feature 16c8cb0ff7 update eventing edit/duplicate to use wizard stepper 2026-07-06 14:16:42 -07:00
its-a-feature ae441b2a3d eventing apitoken scopes and hasura preflight scope checks 2026-07-06 13:12:50 -07:00
its-a-feature c507b99118 reworked operator alias style 2026-07-01 18:54:25 -07:00
github-actions 33a46d7db7 Bump Mythic Dockerfile tag to match release 'v3.4.0.61' 2026-07-01 16:06:33 +00:00
its-a-feature 540aec0f00 fixing filtering bug in process browser 2026-07-01 08:45:06 -07:00
its-a-feature be39f4762b uploaded file edit/save functionality 2026-06-30 16:25:17 -07:00
its-a-feature 7e17721d2d refactored task references and adding @link support 2026-06-29 18:38:00 -07:00
its-a-feature 935b1b023c speed up RPC calls 2026-06-29 16:46:23 -07:00
its-a-feature 58cecc3f86 try to prevent duplicate file uploads 2026-06-29 16:43:51 -07:00
its-a-feature ba68d1aafa adding keyword resolution and reverting to tasks 2026-06-29 09:58:39 -07:00
its-a-feature f4f9f10a6a updated cred parsing, kerberos display, and refactored ticket parsers 2026-06-26 17:36:04 -07:00
its-a-feature 9c8dab4c4a updated kerberos parsing and browser script table text color rendering 2026-06-26 12:33:56 -07:00
its-a-feature 3baef93ed1 updating for credential expansion support 2026-06-25 21:14:11 -07:00
its-a-feature fba421517a updating credential search and tasking processing 2026-06-24 13:35:45 -07:00
its-a-feature 3006594a35 credential and operator alias updates 2026-06-21 10:13:31 -07:00
its-a-feature 07c65b9d65 updated ui colors 2026-06-21 10:13:31 -07:00
its-a-feature 2099875c21 export/import operator aliases 2026-06-21 10:13:31 -07:00
its-a-feature ce7a962d44 add aliases, update tasking for aliases, more relaxed parsing 2026-06-21 10:13:31 -07:00
its-a-feature 2074f51409 updated login page and json inline schema 2026-06-21 10:13:31 -07:00
its-a-feature c1cefea79c initial iteration of UI for Jack's schema viewer 2026-06-21 10:13:31 -07:00
its-a-feature acdd89ca45 display names, choices display names, complex choices support 2026-06-21 10:13:31 -07:00
its-a-feature 3a90a99259 adding complex_choices, other_parameters, and c2 param dynamic queries 2026-06-21 10:13:31 -07:00
its-a-feature b261de26d1 updates for new parameter type 2026-06-21 10:13:30 -07:00
its-a-feature cea78411f5 build parameter groups and normalizing build/c2 parameter features 2026-06-21 10:13:30 -07:00
its-a-feature 6f1e4eac10 hosting files updates 2026-06-21 10:13:30 -07:00
its-a-feature 4f95465278 mcp tool confirmation and scripting normalization 2026-06-21 10:13:30 -07:00
its-a-feature 0a6f30b7da better support chat cancel 2026-06-21 10:13:30 -07:00
its-a-feature 28feedd2bc make and invalidate bot apitokens 2026-06-21 10:13:30 -07:00
its-a-feature 0f2ea3d9cc create and invalidate bot apitokens for chat 2026-06-21 10:13:30 -07:00
its-a-feature 38f201caeb adding authContext to gRPC calls 2026-06-21 10:13:30 -07:00
its-a-feature bab9e55ac4 better markdown/terminal view for task output 2026-06-21 10:13:29 -07:00
its-a-feature 239882474a block list updates, user interact eventing prompts 2026-06-21 10:13:29 -07:00
its-a-feature 43c1ca8429 eventing prompt user input/approval 2026-06-21 10:13:29 -07:00
its-a-feature c57a8fe524 theme updates and better chat config components 2026-06-21 10:13:29 -07:00
its-a-feature 08f84fed4e initial chat feature 2026-06-21 10:13:29 -07:00
its-a-feature 1674f13b1e RabbitMQ Auth Context tracking requirements 2026-06-21 10:13:29 -07:00
its-a-feature 8ae176c49e adjusted reporting page, payloads table, 2026-06-21 10:13:29 -07:00
its-a-feature a332a2332c updated file browser for container files 2026-06-21 10:13:28 -07:00
its-a-feature 01b0c7068e added .dockerignore 2026-06-21 10:13:28 -07:00
its-a-feature b5f982534d better docs pages 2026-06-21 10:13:28 -07:00
its-a-feature 90860793ed fixing sql queries and container down resolver 2026-06-21 10:13:28 -07:00
its-a-feature 68e502ab1c updated installed services table 2026-06-21 10:13:28 -07:00
its-a-feature bd5fa6e0be remove minimap adjust resolution of event log warnings 2026-06-21 10:13:28 -07:00
its-a-feature 21d9368940 per response decode to not bulk bail on bad entries 2026-06-21 10:13:28 -07:00
its-a-feature 862cf8a552 delegate proxy check performance increases 2026-06-21 10:13:28 -07:00
its-a-feature 0225bc7220 c2 diagram updates 2026-06-21 10:13:27 -07:00
its-a-feature fc1944afc9 c2 profile path modernization 2026-06-21 10:13:27 -07:00
its-a-feature c77b71c23a delegate check improvements and process table ui updates 2026-06-21 10:13:27 -07:00
its-a-feature 15bf19a796 better json table displays and search table responses 2026-06-21 10:13:27 -07:00
its-a-feature cde31af97d added file browser insert/delete performance updates 2026-06-21 10:13:27 -07:00
its-a-feature a4a9f141c8 better task completion tracking, proxy tracking, and search page updates 2026-06-21 10:13:27 -07:00
its-a-feature 79c625b7df updated task response iteration and ui task cli updates 2026-06-21 10:13:27 -07:00
its-a-feature 00be046ec3 proxy traffic perf updates and ui parameter dialog modernization 2026-06-21 10:13:27 -07:00
its-a-feature c14933b6b8 better rabbitmq handling and task filter ui updates 2026-06-21 10:13:27 -07:00
its-a-feature e9f70d68ab updates to task and response database tracking 2026-06-21 10:13:26 -07:00
its-a-feature e2545f89b7 updated eventing and installed services tables 2026-06-21 10:13:26 -07:00
its-a-feature a6aeb0a922 more detailed color settings 2026-06-21 10:13:26 -07:00
its-a-feature e623c4fbf2 tag updates 2026-06-21 10:13:26 -07:00
its-a-feature ec345eb4eb updated terminal interactions 2026-06-21 10:13:26 -07:00
its-a-feature 8ce36830e0 remove support for unneeded browserscript result types 2026-06-21 10:13:26 -07:00
its-a-feature 0fccd5a66d updated table filtering 2026-06-21 10:13:26 -07:00
its-a-feature 99ee871383 fixing single task view 2026-06-21 10:13:26 -07:00
its-a-feature b7a5060558 dashboard upates 2026-06-21 10:13:25 -07:00
its-a-feature e89b402b7a eventing updates 2026-06-21 10:13:25 -07:00
its-a-feature f1328a58c4 c2 path and eventing wizard updates 2026-06-21 10:13:25 -07:00
its-a-feature af8fb3c79e callback table row updates 2026-06-21 10:13:25 -07:00
its-a-feature bf33e1fc3a fixed callback table 2026-06-21 10:13:25 -07:00
its-a-feature bfe3d6a732 updating operator settings and virtualized tables 2026-06-21 10:13:25 -07:00
its-a-feature 25795ab9a0 response updates 2026-06-21 10:13:25 -07:00
its-a-feature 64c8fa37ba updated browser script table page 2026-06-21 10:13:25 -07:00
its-a-feature 5c277e7d3c apitoken create updates 2026-06-21 10:13:25 -07:00
its-a-feature f1c7601280 updated single task view page 2026-06-21 10:13:25 -07:00
its-a-feature 269dc0a41f updated builder and eventing pages 2026-06-21 10:13:24 -07:00
its-a-feature db23b47074 updating some of the modal rendering conditionals 2026-06-21 10:13:24 -07:00
its-a-feature 132b0416ee major updates of the search, eventing, and payloads pages 2026-06-21 10:13:24 -07:00
its-a-feature a8729ff6f2 updating gitignore 2026-06-21 10:13:24 -07:00
its-a-feature cfb31788f2 updating datagrid 2026-06-21 10:13:24 -07:00
its-a-feature 5a20b2d86f "modernizing" of the UI components 2026-06-21 10:13:24 -07:00
its-a-feature 7d52dbebcd adding xterm for interactive tasking responses 2026-06-21 10:13:24 -07:00
its-a-feature df392eb4e0 better column dragging for virtualized tables 2026-06-21 10:13:24 -07:00
its-a-feature 57b6bd5ee8 better scrolling for interactive tasks 2026-06-21 10:13:23 -07:00
its-a-feature 3ac9761b39 better handling of pagination and streaming responses 2026-06-21 10:13:23 -07:00
its-a-feature 0927c67734 updating hasura actions to all be camelCase 2026-06-21 10:13:23 -07:00
its-a-feature c9d7392d29 updated scope tracking 2026-06-21 10:13:23 -07:00
its-a-feature c369aec44c additional scopes for apitokens and their checks 2026-06-21 10:13:23 -07:00
its-a-feature 0c8c558a05 grpc typo fix 2026-06-21 10:12:48 -07:00
its-a-feature 5c61fb9c2e prevent duplicate onStart calls for the same container 2026-06-21 10:12:48 -07:00
its-a-feature 5606d077d5 updated lock synchronization 2026-06-21 10:12:48 -07:00
its-a-feature 4e10fb6f50 logging updates and more operation scope checks added to routes 2026-06-21 10:12:48 -07:00
its-a-feature 11376d1ad2 more api validation and logging updates 2026-06-21 10:11:06 -07:00
its-a-feature ef6cfa2644 updating apitokens to be different, removing /api/v1.4 prefix, scopes 2026-06-21 10:10:30 -07:00
its-a-feature 437d5f58f9 starting auth adjustments to remove cookies 2026-06-21 10:10:30 -07:00
github-actions 29be09eef4 Bump Mythic Dockerfile tag to match release 'v3.4.0.60' 2026-06-21 17:04:09 +00:00
its-a-feature a5a1a972d7 updating versioning 2026-06-21 09:09:15 -07:00
its-a-feature 82648e8241 addressing issues #563 #564 and #565 2026-06-21 09:04:45 -07:00
github-actions e4da2c887f Bump Mythic Dockerfile tag to match release 'v3.4.0.59' 2026-06-04 18:25:04 +00:00
Cody Thomas 39f44f7dec Merge pull request #557 from anuj-modi/internal-registry-support
Internal Registry Support
2026-06-04 12:54:16 -05:00
Anuj Modi 0f51584493 add some logs so users know Mirror is configured 2026-05-27 10:35:40 -04:00
Anuj Modi dc50ecc0dc refactor to one variable rather than 2 2026-05-26 16:06:20 -04:00
Anuj Modi 59211a2fbf Add support for Interal Registries for Core images and Agents 2026-05-21 15:46:53 -04:00
its-a-feature f8d355a29c updating single task view 2026-05-07 16:16:32 -05:00
github-actions 1da8fdcd2a Bump Mythic Dockerfile tag to match release 'v3.4.0.58' 2026-05-07 20:08:16 +00:00
its-a-feature 2dcc3b5699 fixing lazyQuery on single task view page 2026-05-07 14:47:22 -05:00
Cody Thomas 8deec59270 Merge pull request #548 from s1nc3ju1y/fix-jupyter-port-env
Pass JUPYTER_PORT into mythic_jupyter container
2026-04-28 13:58:22 -05:00
s1nc3ju1y 071abeaf74 Pass JUPYTER_PORT into mythic_jupyter container 2026-04-28 19:32:05 +08:00
its-a-feature 6eb53da6b5 slight UI updates, additional color choice added, some formatting 2026-04-16 17:39:11 -05:00
github-actions d605a2f82c Bump Mythic Dockerfile tag to match release 'v3.4.0.57' 2026-04-16 13:28:59 +00:00
its-a-feature 31db999945 updating version and readme 2026-04-16 08:07:33 -05:00
Cody Thomas 9ae193be76 Merge pull request #546 from Kargha/fix-missing-return-apitokengen
fix missing return in GenerateAPITokenWebhook
2026-04-16 07:59:01 -05:00
Andreas Persson fc731af819 fix missing return in GenerateAPITokenWebhook 2026-04-16 12:17:38 +09:00
Cody Thomas d21dbf293b Merge pull request #541 from its-a-feature/dependabot/go_modules/Mythic_CLI/src/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp-1.43.0
Bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp from 1.25.0 to 1.43.0 in /Mythic_CLI/src
2026-04-15 16:13:55 -05:00
dependabot[bot] b6d98b1794 Bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
Bumps [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp](https://github.com/open-telemetry/opentelemetry-go) from 1.25.0 to 1.43.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.25.0...v1.43.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
  dependency-version: 1.43.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-04-15 21:11:32 +00:00
Cody Thomas 448a57b82b Merge pull request #542 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/lodash-4.18.1
Bump lodash from 4.17.23 to 4.18.1 in /MythicReactUI
2026-04-15 16:10:35 -05:00
Cody Thomas 1d54d5ea0c Merge pull request #543 from its-a-feature/dependabot/go_modules/Mythic_CLI/src/go.opentelemetry.io/otel/sdk-1.43.0
Bump go.opentelemetry.io/otel/sdk from 1.42.0 to 1.43.0 in /Mythic_CLI/src
2026-04-15 16:10:14 -05:00
Cody Thomas 4d793d5998 Merge pull request #544 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/follow-redirects-1.16.0
Bump follow-redirects from 1.15.9 to 1.16.0 in /MythicReactUI
2026-04-15 16:09:29 -05:00
its-a-feature 60455c26cb add mythic server name to emitted log messages 2026-04-15 08:34:52 -05:00
Cody Thomas 183f0605c2 Merge pull request #545 from Kargha/fix-emit-task-log-operation-id
Fix OperationID always being 0 in emitTaskLog
2026-04-15 08:20:36 -05:00
Andreas Persson 37d3cc8b1b Fix OperationID always being 0 in emitTaskLog 2026-04-15 21:26:48 +09:00
dependabot[bot] 9532b96f7a Bump follow-redirects from 1.15.9 to 1.16.0 in /MythicReactUI
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.9 to 1.16.0.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.9...v1.16.0)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-version: 1.16.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-04-15 11:12:15 +00:00
its-a-feature e1806f0fd5 updating event log message handling for performance updates 2026-04-13 09:08:13 -05:00
github-actions fa235b6378 Bump Mythic Dockerfile tag to match release 'v3.4.0.56' 2026-04-10 15:36:04 +00:00
its-a-feature b87219b1e9 updating c2 profile build to leverage ui_position specifications 2026-04-10 10:14:05 -05:00
github-actions b5459b56c8 Bump Mythic Dockerfile tag to match release 'v3.4.0.55' 2026-04-10 14:57:33 +00:00
its-a-feature ca6d7d2ce0 fixed an issue with database connection exhaustion on errors 2026-04-10 09:35:59 -05:00
github-actions 6148c5f0fb Bump Mythic Dockerfile tag to match release 'v3.4.0.54' 2026-04-10 13:42:54 +00:00
its-a-feature 6e14b2a472 updating postgres healthcheck 2026-04-10 08:21:55 -05:00
dependabot[bot] 307df57df8 Bump go.opentelemetry.io/otel/sdk in /Mythic_CLI/src
Bumps [go.opentelemetry.io/otel/sdk](https://github.com/open-telemetry/opentelemetry-go) from 1.42.0 to 1.43.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.42.0...v1.43.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/sdk
  dependency-version: 1.43.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-04-08 20:27:11 +00:00
dependabot[bot] 4a4d95f1ca Bump lodash from 4.17.23 to 4.18.1 in /MythicReactUI
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.23 to 4.18.1.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.18.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-04-08 20:23:39 +00:00
github-actions ffc5041349 Bump Mythic Dockerfile tag to match release 'v3.4.0.53' 2026-04-06 21:49:43 +00:00
its-a-feature ecda245963 updating version tracking for merged PR 2026-04-06 16:27:36 -05:00
Cody Thomas 6ea67c69b9 Merge pull request #539 from nt0xa/auth_acs-get-handler
GET request handler for /auth_acs/:containerName/:IDPName
2026-04-06 16:25:10 -05:00
Anton Prokhorov 64d32eb116 GET request handler for /auth_acs/:containerName/:IDPName 2026-04-06 22:11:22 +01:00
Cody Thomas 9f7a1ce770 Merge pull request #537 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/node-forge-1.4.0
Bump node-forge from 1.3.2 to 1.4.0 in /MythicReactUI
2026-03-27 13:40:12 -05:00
dependabot[bot] 4a3818988c Bump node-forge from 1.3.2 to 1.4.0 in /MythicReactUI
Bumps [node-forge](https://github.com/digitalbazaar/forge) from 1.3.2 to 1.4.0.
- [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/v1.3.2...v1.4.0)

---
updated-dependencies:
- dependency-name: node-forge
  dependency-version: 1.4.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-03-27 18:39:09 +00:00
Cody Thomas 1ec6318b62 Merge pull request #536 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/picomatch-2.3.2
Bump picomatch from 2.3.1 to 2.3.2 in /MythicReactUI
2026-03-27 13:38:39 -05:00
Cody Thomas 60902a6e66 Merge pull request #535 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/multi-c136cad177
Bump yaml in /MythicReactUI
2026-03-27 13:38:11 -05:00
dependabot[bot] ddda1a266c Bump picomatch from 2.3.1 to 2.3.2 in /MythicReactUI
Bumps [picomatch](https://github.com/micromatch/picomatch) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-03-25 22:25:24 +00:00
dependabot[bot] 8dc9f351d9 Bump yaml in /MythicReactUI
Bumps  and [yaml](https://github.com/eemeli/yaml). These dependencies needed to be updated together.

Updates `yaml` from 1.10.2 to 1.10.3
- [Release notes](https://github.com/eemeli/yaml/releases)
- [Commits](https://github.com/eemeli/yaml/compare/v1.10.2...v1.10.3)

Updates `yaml` from 2.7.1 to 2.8.3
- [Release notes](https://github.com/eemeli/yaml/releases)
- [Commits](https://github.com/eemeli/yaml/compare/v1.10.2...v1.10.3)

---
updated-dependencies:
- dependency-name: yaml
  dependency-version: 1.10.3
  dependency-type: indirect
- dependency-name: yaml
  dependency-version: 2.8.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-03-25 20:21:11 +00:00
github-actions d7b4d51186 Bump Mythic Dockerfile tag to match release 'v3.4.0.52' 2026-03-24 13:42:04 +00:00
its-a-feature d2334b79de bumping dependencies 2026-03-24 08:20:20 -05:00
Cody Thomas 96df4db36e Merge pull request #534 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/jsonpath-1.3.0
Bump jsonpath from 1.2.1 to 1.3.0 in /MythicReactUI
2026-03-24 08:09:35 -05:00
dependabot[bot] a72dd1b086 Bump jsonpath from 1.2.1 to 1.3.0 in /MythicReactUI
Bumps [jsonpath](https://github.com/dchester/jsonpath) from 1.2.1 to 1.3.0.
- [Commits](https://github.com/dchester/jsonpath/commits)

---
updated-dependencies:
- dependency-name: jsonpath
  dependency-version: 1.3.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-03-24 13:08:48 +00:00
Cody Thomas 89bee21894 Merge pull request #529 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/flatted-3.4.2
Bump flatted from 3.3.3 to 3.4.2 in /MythicReactUI
2026-03-24 08:08:11 -05:00
Cody Thomas 26a4410f0a Merge pull request #527 from its-a-feature/dependabot/go_modules/mythic-docker/src/google.golang.org/grpc-1.79.3
Bump google.golang.org/grpc from 1.79.2 to 1.79.3 in /mythic-docker/src
2026-03-24 08:07:51 -05:00
github-actions 36fb4c58bf Bump Mythic Dockerfile tag to match release 'v3.4.0.51' 2026-03-23 15:30:55 +00:00
its-a-feature cfdf0c537d Merge remote-tracking branch 'origin/master' 2026-03-23 10:09:04 -05:00
its-a-feature f23e123a56 fixing file browser bug, upload bug, and file removal bug 2026-03-23 10:08:57 -05:00
github-actions 5b4c210e6f Bump Mythic Dockerfile tag to match release 'v3.4.0.50' 2026-03-23 14:06:16 +00:00
its-a-feature 3dee8797da updating to fix file click issue 2026-03-21 14:53:40 -05:00
dependabot[bot] 33f48baab4 Bump flatted from 3.3.3 to 3.4.2 in /MythicReactUI
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.3 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-03-21 10:30:41 +00:00
github-actions 3198f304c3 Bump Mythic Dockerfile tag to match release 'v3.4.0.49' 2026-03-20 23:03:14 +00:00
its-a-feature 09ac733d45 UI updates and back-end better file transfer tracking 2026-03-20 17:27:25 -05:00
its-a-feature 3e611494cc First set of draggable modals to access content behind them 2026-03-19 19:05:33 -05:00
dependabot[bot] 8be3c084e5 Bump google.golang.org/grpc from 1.79.2 to 1.79.3 in /mythic-docker/src
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.79.2 to 1.79.3.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.79.2...v1.79.3)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.79.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-03-19 02:35:13 +00:00
github-actions 3796640835 Bump Mythic Dockerfile tag to match release 'v3.4.0.48' 2026-03-18 20:17:23 +00:00
its-a-feature 6e23b29f22 tweaking how MythicRPCFileSearch works to prevent duplicates 2026-03-18 14:55:38 -05:00
github-actions 22e5c489b2 Bump Mythic Dockerfile tag to match release 'v3.4.0.47' 2026-03-17 17:18:52 +00:00
its-a-feature 8cdf34c3e7 fixing bug with file uploads in recreated build parameters 2026-03-17 11:22:41 -05:00
its-a-feature b285558fce fixing bug with nil path for screenshots 2026-03-17 08:26:34 -05:00
its-a-feature e536f59347 updating mythic-cli go docker version 2026-03-17 07:57:54 -05:00
its-a-feature 4c472f7e3a updating go modules for mythic_server and mythic-cli 2026-03-16 14:31:34 -05:00
Cody Thomas cc77f2b1a3 Merge pull request #525 from its-a-feature/dependabot/go_modules/Mythic_CLI/src/golang.org/x/net-0.38.0
Bump golang.org/x/net from 0.28.0 to 0.38.0 in /Mythic_CLI/src
2026-03-16 09:58:23 -05:00
dependabot[bot] 1c18703e96 Bump golang.org/x/net from 0.28.0 to 0.38.0 in /Mythic_CLI/src
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.28.0 to 0.38.0.
- [Commits](https://github.com/golang/net/compare/v0.28.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.38.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-03-16 14:57:34 +00:00
Cody Thomas f017661b3d Merge pull request #520 from its-a-feature/dependabot/go_modules/Mythic_CLI/src/go.opentelemetry.io/otel/sdk-1.40.0
Bump go.opentelemetry.io/otel/sdk from 1.25.0 to 1.40.0 in /Mythic_CLI/src
2026-03-16 09:56:49 -05:00
Cody Thomas 98dd30d984 Merge pull request #501 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/multi-75e6bc5210
Bump js-yaml in /MythicReactUI
2026-03-16 09:55:30 -05:00
Cody Thomas eb3cf1053b Merge pull request #524 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/multi-b5ddb13e79
Bump react-router and react-router-dom in /MythicReactUI
2026-03-16 09:54:19 -05:00
Cody Thomas 58e2c6de5f Merge pull request #522 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/ajv-6.14.0
Bump ajv from 6.12.6 to 6.14.0 in /MythicReactUI
2026-03-16 09:53:17 -05:00
Cody Thomas 7dc66c43aa Merge pull request #521 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/rollup-2.80.0
Bump rollup from 2.79.2 to 2.80.0 in /MythicReactUI
2026-03-16 09:52:53 -05:00
dependabot[bot] 9c6afe2d8c Bump react-router and react-router-dom in /MythicReactUI
Bumps [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) to 7.13.1 and updates ancestor dependency [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom). These dependencies need to be updated together.


Updates `react-router` from 7.5.2 to 7.13.1
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/[email protected]/packages/react-router)

Updates `react-router-dom` from 7.5.2 to 7.13.1
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/[email protected]/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router
  dependency-version: 7.13.1
  dependency-type: indirect
- dependency-name: react-router-dom
  dependency-version: 7.13.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-03-16 14:50:21 +00:00
dependabot[bot] 6c5b1d2a24 Bump ajv from 6.12.6 to 6.14.0 in /MythicReactUI
Bumps [ajv](https://github.com/ajv-validator/ajv) from 6.12.6 to 6.14.0.
- [Release notes](https://github.com/ajv-validator/ajv/releases)
- [Commits](https://github.com/ajv-validator/ajv/compare/v6.12.6...v6.14.0)

---
updated-dependencies:
- dependency-name: ajv
  dependency-version: 6.14.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-03-16 14:50:01 +00:00
dependabot[bot] fc8239eb85 Bump rollup from 2.79.2 to 2.80.0 in /MythicReactUI
Bumps [rollup](https://github.com/rollup/rollup) from 2.79.2 to 2.80.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/v2.80.0/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.79.2...v2.80.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 2.80.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-03-16 14:49:56 +00:00
Cody Thomas 180c6e1914 Merge pull request #511 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/lodash-4.17.23
Bump lodash from 4.17.21 to 4.17.23 in /MythicReactUI
2026-03-16 09:49:35 -05:00
Cody Thomas 59c97d791f Merge pull request #523 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/multi-ca49cfd856
Bump minimatch in /MythicReactUI
2026-03-16 09:49:10 -05:00
Cody Thomas d9e1d5e6bd Merge pull request #497 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/node-forge-1.3.2
Bump node-forge from 1.3.1 to 1.3.2 in /MythicReactUI
2026-03-16 09:48:47 -05:00
Cody Thomas 0f5746b4a3 Merge pull request #514 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/webpack-5.105.0
Bump webpack from 5.99.6 to 5.105.0 in /MythicReactUI
2026-03-16 09:48:27 -05:00
dependabot[bot] 71c03251fb Bump minimatch in /MythicReactUI
Bumps  and [minimatch](https://github.com/isaacs/minimatch). These dependencies needed to be updated together.

Updates `minimatch` from 3.1.2 to 3.1.5
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5)

Updates `minimatch` from 9.0.5 to 9.0.9
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5)

Updates `minimatch` from 5.1.6 to 5.1.9
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
- dependency-name: minimatch
  dependency-version: 9.0.9
  dependency-type: indirect
- dependency-name: minimatch
  dependency-version: 5.1.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-03-16 14:48:19 +00:00
dependabot[bot] f742ab3d0f Bump go.opentelemetry.io/otel/sdk in /Mythic_CLI/src
Bumps [go.opentelemetry.io/otel/sdk](https://github.com/open-telemetry/opentelemetry-go) from 1.25.0 to 1.40.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.25.0...v1.40.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/sdk
  dependency-version: 1.40.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-03-16 14:48:09 +00:00
Cody Thomas d3ae12a220 Merge pull request #515 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/jsonpath-1.2.1
Bump jsonpath from 1.1.1 to 1.2.1 in /MythicReactUI
2026-03-16 09:47:31 -05:00
Cody Thomas d9a41572ac Merge pull request #517 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/multi-a07fd7252a
Bump qs and express in /MythicReactUI
2026-03-16 09:47:07 -05:00
github-actions e454531999 Bump Mythic Dockerfile tag to match release 'v3.4.0.46' 2026-03-16 14:45:43 +00:00
its-a-feature 45cf6a9cd8 fixing bug from bad staging 2026-03-16 09:24:21 -05:00
github-actions 5011314ba5 Bump Mythic Dockerfile tag to match release 'v3.4.0.45' 2026-03-12 20:32:09 +00:00
its-a-feature af9d3c40e6 fixing callback imports and scrolling pages/tabs 2026-03-12 15:12:15 -05:00
github-actions 03e27abdb6 Bump Mythic Dockerfile tag to match release 'v3.4.0.44' 2026-03-05 00:04:17 +00:00
its-a-feature e7fde3fcde adding burst reads to rpfwd 2026-03-04 17:44:44 -06:00
its-a-feature 51c0eeb5df updating readme 2026-02-19 12:43:57 -06:00
github-actions 0f259d1f7d Bump Mythic Dockerfile tag to match release 'v3.4.0.43' 2026-02-19 17:38:14 +00:00
its-a-feature d86f0d6227 fixing ui bug with database query on file preview 2026-02-19 11:18:42 -06:00
its-a-feature 3bb93efa72 pushC2 and proxy updates 2026-02-18 13:53:14 -06:00
Cody Thomas 652234d7e1 Merge pull request #516 from CCob/grpc-corruption
This PR fixes a PushC2 gRPC stream corruption problem when SOCKS traffic creates heavy load
2026-02-16 09:33:56 -06:00
dependabot[bot] 44d8fd168e Bump qs and express in /MythicReactUI
Bumps [qs](https://github.com/ljharb/qs) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `qs` from 6.13.0 to 6.14.2
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.13.0...v6.14.2)

Updates `express` from 4.21.2 to 4.22.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/v4.22.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.2...v4.22.1)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.14.2
  dependency-type: indirect
- dependency-name: express
  dependency-version: 4.22.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-02-14 03:50:56 +00:00
Ceri Coburn 2e418bbb4d Increase the per agent C2 channel message queue to 1000 to reduce the chances of a full queue under load 2026-02-13 13:01:14 +00:00
Ceri Coburn 87249b2891 Fail immediately and drop SOCKS messages when the gRPC channel is full
The previous wait for 10s will block ALL SOCKS packets across ALL agents due to a single agents full push queue. This is due to the synchronous nature of interceptProxyDataToAgentForPushC2. If we drop immediately due to a single busy/slow agent , we don't affect the others.
2026-02-13 13:00:31 +00:00
Ceri Coburn dbb15fdbb3 Put a mutex around stream.Send
Since stream.Send is called within an async goroutine and in the outer StartPushC2Streaming method, this can cause corruption on the gRPC stream since two sends can potentially be sent at the same time during heavy load
2026-02-13 12:56:38 +00:00
dependabot[bot] 04ebb6aba7 Bump jsonpath from 1.1.1 to 1.2.1 in /MythicReactUI
Bumps [jsonpath](https://github.com/dchester/jsonpath) from 1.1.1 to 1.2.1.
- [Commits](https://github.com/dchester/jsonpath/commits/1.2.1)

---
updated-dependencies:
- dependency-name: jsonpath
  dependency-version: 1.2.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-02-12 15:31:41 +00:00
github-actions 8a2e42c48c Bump Mythic Dockerfile tag to match release 'v3.4.0.42' 2026-02-10 15:37:16 +00:00
its-a-feature db682d5020 updating file/process browser improvements 2026-02-10 09:17:14 -06:00
dependabot[bot] 6649279e3e Bump webpack from 5.99.6 to 5.105.0 in /MythicReactUI
Bumps [webpack](https://github.com/webpack/webpack) from 5.99.6 to 5.105.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Changelog](https://github.com/webpack/webpack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack/compare/v5.99.6...v5.105.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-version: 5.105.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-02-07 14:17:12 +00:00
github-actions 6d1778bb3b Bump Mythic Dockerfile tag to match release 'v3.4.0.41' 2026-02-06 14:44:14 +00:00
its-a-feature 0484154eae few UI and back-end fixes 2026-02-06 08:24:31 -06:00
github-actions 99f2b6cb2f Bump Mythic Dockerfile tag to match release 'v3.4.0.40' 2026-02-02 23:31:17 +00:00
its-a-feature 303741ee28 updating browser script table sorting 2026-02-02 14:24:57 -06:00
github-actions 37a3f66d68 Bump Mythic Dockerfile tag to match release 'v3.4.0.39' 2026-02-02 17:58:58 +00:00
its-a-feature 8e43275c27 p2p fixes and building/c2 syncing fixes 2026-02-02 11:39:44 -06:00
github-actions 6dfe40b274 Bump Mythic Dockerfile tag to match release 'v3.4.0.38' 2026-01-30 13:46:45 +00:00
its-a-feature 49e1c62502 adding supported_ui_feature of file_browser:remove_folder 2026-01-30 07:28:00 -06:00
github-actions a739837c95 Bump Mythic Dockerfile tag to match release 'v3.4.0.37' 2026-01-29 22:21:24 +00:00
its-a-feature 063f70a55d updating Mythic UI to add in tabs browser script option 2026-01-29 16:02:06 -06:00
github-actions fb6c3d8368 Bump Mythic Dockerfile tag to match release 'v3.4.0.36' 2026-01-27 19:39:24 +00:00
its-a-feature 10f0eb9e7a fix for UI caching bug in split tasking view 2026-01-27 13:20:15 -06:00
dependabot[bot] afdd0ec9cc Bump lodash from 4.17.21 to 4.17.23 in /MythicReactUI
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-01-21 23:47:42 +00:00
github-actions 2391244baa Bump Mythic Dockerfile tag to match release 'v3.4.0.35' 2026-01-14 22:51:11 +00:00
its-a-feature 9e0fbcfa3a more UI config options and a UI streaming update fix 2026-01-14 16:31:48 -06:00
github-actions 54e6cc5d32 Bump Mythic Dockerfile tag to match release 'v3.4.0.34' 2026-01-09 19:14:06 +00:00
its-a-feature c56d92ee52 UI fix for dynamic query parameters 2026-01-09 10:53:35 -08:00
github-actions 706e8d3ce7 Bump Mythic Dockerfile tag to match release 'v3.4.0.33' 2026-01-09 05:39:11 +00:00
its-a-feature 29e57f1384 Fixing UI bug with C2 deviations 2026-01-08 21:20:13 -08:00
its-a-feature bf7de9ad22 adjust docker health check start times 2026-01-04 08:31:28 -06:00
github-actions e3cbdd35e7 Bump Mythic Dockerfile tag to match release 'v3.4.0.32' 2026-01-02 17:19:53 +00:00
its-a-feature 2ca4ad507e updating jupyter and container orders 2026-01-02 10:51:56 -06:00
github-actions 43a8a8580c Bump Mythic Dockerfile tag to match release 'v3.4.0.31' 2026-01-01 16:08:13 +00:00
its-a-feature fd02fe2c9e adding more scripting examples to command info 2026-01-01 09:49:30 -06:00
github-actions 3a6d9c4842 Bump Mythic Dockerfile tag to match release 'v3.4.0.30' 2025-12-31 20:15:39 +00:00
its-a-feature 31e297a37f fixing UI bugs, remove dangling images in docker by default, skip steps 2025-12-31 13:54:23 -06:00
its-a-feature b40aed02b8 fixing some UI issues 2025-12-29 11:41:52 -06:00
github-actions 5e031f64b2 Bump Mythic Dockerfile tag to match release 'v3.4.0.29' 2025-12-23 21:43:19 +00:00
its-a-feature f6532b127d updating processing for file chunked browser data 2025-12-23 15:23:43 -06:00
github-actions a383fcf8c5 Bump Mythic Dockerfile tag to match release 'v3.4.0.28' 2025-12-23 16:19:35 +00:00
its-a-feature 307eb0dd72 fixing file browser bug 2025-12-23 10:00:23 -06:00
github-actions 29db86be7c Bump Mythic Dockerfile tag to match release 'v3.4.0.27' 2025-12-23 14:21:51 +00:00
its-a-feature 04be0065f3 fixing issue with cron parsing 2025-12-23 08:02:57 -06:00
github-actions 8601357379 Bump Mythic Dockerfile tag to match release 'v3.4.0.26' 2025-12-22 20:23:57 +00:00
its-a-feature bb3a4b2294 fixing syncing bug with translation containers 2025-12-22 14:05:05 -06:00
github-actions 44149bdf2b Bump Mythic Dockerfile tag to match release 'v3.4.0.25' 2025-12-18 20:26:47 +00:00
its-a-feature 1a2bf56c8c adding more database triggers for updating timestamps 2025-12-18 14:07:56 -06:00
github-actions 63867bc27a Bump Mythic Dockerfile tag to match release 'v3.4.0.24' 2025-12-15 20:29:10 +00:00
its-a-feature 42a7869750 updating default UI and change to RPC Task Create 2025-12-15 14:08:09 -06:00
github-actions 8479a2d091 Bump Mythic Dockerfile tag to match release 'v3.4.0.23' 2025-12-11 17:42:46 +00:00
its-a-feature c7357c6fee updating custom browser functionality 2025-12-11 11:23:57 -06:00
github-actions e6fd09b069 Bump Mythic Dockerfile tag to match release 'v3.4.0.22' 2025-12-10 20:42:36 +00:00
its-a-feature 8cc47d518e updating UI side bar ordering of CSS 2025-12-10 14:17:54 -06:00
github-actions ae2ed75801 Bump Mythic Dockerfile tag to match release 'v3.4.0.21' 2025-12-10 19:40:46 +00:00
dependabot[bot] 7f225e51f1 Bump js-yaml in /MythicReactUI
Bumps  and [js-yaml](https://github.com/nodeca/js-yaml). These dependencies needed to be updated together.

Updates `js-yaml` from 3.14.1 to 3.14.2
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.14.1...3.14.2)

Updates `js-yaml` from 4.1.0 to 4.1.1
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.14.1...3.14.2)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 3.14.2
  dependency-type: indirect
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-12-10 19:22:35 +00:00
Cody Thomas c173f083dd Merge pull request #500 from its-a-feature/custombrowser
Custombrowser
2025-12-10 13:21:32 -06:00
its-a-feature 3e3356e586 updating changelog 2025-12-10 13:21:04 -06:00
its-a-feature 70a4ca26ec adding changelog 2025-12-10 13:17:15 -06:00
its-a-feature 64fd83b999 adding vhost options 2025-12-10 12:03:49 -06:00
its-a-feature 180a81111d adding in mythic_vhost 2025-12-10 11:55:23 -06:00
its-a-feature 767881fd6c updating container version semver to be more explicit 2025-12-10 09:49:14 -06:00
its-a-feature 4240722b3a updated changelog/readme 2025-12-10 09:16:57 -06:00
its-a-feature 36ceaaacba custom browser update 2025-12-10 08:20:43 -06:00
dependabot[bot] bf22f85661 Bump node-forge from 1.3.1 to 1.3.2 in /MythicReactUI
Bumps [node-forge](https://github.com/digitalbazaar/forge) from 1.3.1 to 1.3.2.
- [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/v1.3.1...v1.3.2)

---
updated-dependencies:
- dependency-name: node-forge
  dependency-version: 1.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-11-26 22:19:13 +00:00
github-actions e7405bd62c Bump Mythic Dockerfile tag to match release 'v3.4.0.20' 2025-11-10 16:21:53 +00:00
its-a-feature 2ab6e20162 updated ui to fix bug with no c2 profile 2025-11-10 10:01:34 -06:00
github-actions 274e41f1ee Bump Mythic Dockerfile tag to match release 'v3.4.0.19' 2025-10-31 14:56:43 +00:00
its-a-feature 3748d68f2e fix for c2 logo tracking 2025-10-31 09:36:32 -05:00
github-actions e8efb8acea Bump Mythic Dockerfile tag to match release 'v3.4.0.18' 2025-10-30 16:59:53 +00:00
its-a-feature 438bbf1b3d fixing eventing bug 2025-10-30 11:40:12 -05:00
its-a-feature 5812391061 updating readme badges 2025-10-30 10:20:24 -05:00
github-actions 07e6bedf71 Bump Mythic Dockerfile tag to match release 'v3.4.0.17' 2025-10-29 14:31:55 +00:00
its-a-feature 1966848577 adding two new MythicRPC endpoints 2025-10-29 08:33:56 -05:00
github-actions 3953850496 Bump Mythic Dockerfile tag to match release 'v3.4.0.16' 2025-10-28 19:31:10 +00:00
its-a-feature aa1de9ef14 fixing UI bug for hidden parameters 2025-10-28 14:11:36 -05:00
github-actions 7dd545fb9d Bump Mythic Dockerfile tag to match release 'v3.4.0.15' 2025-10-28 15:18:02 +00:00
its-a-feature c997ab3a8b updating migrations 2025-10-28 09:56:59 -05:00
its-a-feature 7e95ae879d updating migrations 2025-10-28 09:45:05 -05:00
its-a-feature 5d4c173099 updating mythic-cli version 2025-10-28 09:33:52 -05:00
Cody Thomas 8095eee496 Merge pull request #489 from Norton1999/master
fix when installLocation is an ssh link
2025-10-28 09:32:20 -05:00
its-a-feature 9ed4a8ee49 new build ordering and dynamic functions 2025-10-28 09:31:32 -05:00
github-actions 520e3a94ca Bump Mythic Dockerfile tag to match release 'v3.4.0.14' 2025-10-16 21:20:48 +00:00
its-a-feature 9a44032528 forgot to update the version 2025-10-16 23:01:02 +02:00
its-a-feature df8b903757 fixing selection bug 2025-10-16 22:56:42 +02:00
github-actions 3a814eca14 Bump Mythic Dockerfile tag to match release 'v3.4.0.13' 2025-10-16 20:41:50 +00:00
its-a-feature d169ee834e Merge remote-tracking branch 'origin/master' 2025-10-16 22:19:04 +02:00
its-a-feature c7dab9a565 fixing a UI bug for CLI quotes and file browser permissions display 2025-10-16 22:19:00 +02:00
github-actions 1e0202eaba Bump Mythic Dockerfile tag to match release 'v3.4.0.12' 2025-10-16 09:38:47 +00:00
its-a-feature 405f58c68e fixing bug with dictionary c2 deviations in scripting defaults 2025-10-16 11:16:12 +02:00
its-a-feature ba593a8f94 fixing issue with filtered command attributes in task modals 2025-10-16 04:21:53 +02:00
github-actions ebf6425fd9 Bump Mythic Dockerfile tag to match release 'v3.4.0.11' 2025-10-16 02:00:01 +00:00
its-a-feature 8903cf243a fixing an issue with generating api tokens 2025-10-16 03:40:45 +02:00
Nellie 6333fef797 fix when installLocation is an ssh link 2025-10-15 18:21:58 -07:00
github-actions cf557df672 Bump Mythic Dockerfile tag to match release 'v3.4.0.10' 2025-10-13 20:14:12 +00:00
its-a-feature c2bbc061bf fixing an issue with callbackgraph searching 2025-10-13 14:53:20 -05:00
github-actions af5f4416a6 Bump Mythic Dockerfile tag to match release 'v3.4.0.9' 2025-10-13 14:27:30 +00:00
its-a-feature 0692cad336 fixing dictionary parameter deviations 2025-10-13 09:07:46 -05:00
github-actions e244f9fcd1 Bump Mythic Dockerfile tag to match release 'v3.4.0.8' 2025-10-11 15:31:01 +00:00
its-a-feature dd8c548242 UI fixes for files/booleans with payloads 2025-10-11 10:11:56 -05:00
github-actions 137b5cc4a1 Bump Mythic Dockerfile tag to match release 'v3.4.0.7' 2025-10-10 17:04:33 +00:00
its-a-feature 92f3ee46b6 updating jupyter pypi scripting to 0.2.10 2025-10-10 11:44:33 -05:00
github-actions 8979bd1f42 Bump Mythic Dockerfile tag to match release 'v3.4.0.6' 2025-10-10 16:10:45 +00:00
its-a-feature 46abe5a032 few small bug fixes 2025-10-10 10:49:22 -05:00
github-actions b294c8ff53 Bump Mythic Dockerfile tag to match release 'v3.4.0.5' 2025-10-09 16:38:29 +00:00
its-a-feature 11ab25a74e fixing linking/tagging bugs and updating golang json 2025-10-09 10:01:51 -05:00
github-actions 33d0e0b7a3 Bump Mythic Dockerfile tag to match release 'v3.4.0.3' 2025-10-08 22:02:10 +00:00
its-a-feature 48c77ed20b few UI fixes for payload builds 2025-10-08 16:42:27 -05:00
github-actions 64828ca26e Bump Mythic Dockerfile tag to match release 'v3.4.0.2' 2025-10-07 23:35:23 +00:00
its-a-feature 96231b6f5e updating ui for better icon placement for c2 profiles 2025-10-07 18:16:07 -05:00
github-actions f1c1ceca91 Bump Mythic Dockerfile tag to match release 'v3.4.0.1' 2025-10-07 19:14:31 +00:00
its-a-feature 2826b39c90 fix a bug where web response for test could be nil 2025-10-07 13:55:12 -05:00
its-a-feature 14e5a10a9a adding translation container semver 2025-10-07 10:57:21 -05:00
its-a-feature 675827ecce updating migration version 2025-10-07 10:53:22 -05:00
Cody Thomas 599cb57682 Merge pull request #487 from its-a-feature/dynamic_params
Dynamic params
2025-10-07 10:28:56 -05:00
its-a-feature 2bd5da93ff updating changelog date 2025-10-07 10:28:02 -05:00
its-a-feature 4896194b0c finalizing dynamic parameters and ui updates for building 2025-10-07 09:58:48 -05:00
its-a-feature fae956bec4 updating for new global setting 2025-09-11 13:45:06 -05:00
its-a-feature 09e0b011f1 adding global setting for webhooks 2025-09-11 12:01:45 -05:00
its-a-feature 62c60601c3 ui updates for payload/wrapper builds 2025-09-11 10:38:48 -05:00
its-a-feature 461c6c689e payload building changes 2025-09-08 16:50:30 -05:00
its-a-feature b7d113753f more updates for new and dynamic build process 2025-09-05 09:06:42 -05:00
its-a-feature 1f274b0697 initial dynamic params update 2025-09-05 09:06:42 -05:00
github-actions c52a6c66e9 Bump Mythic Dockerfile tag to match release 'v3.3.0.136' 2025-09-05 13:36:21 +00:00
its-a-feature 74864c1803 fixing bug in staging_translation 2025-09-05 08:18:22 -05:00
github-actions 364f87a6f2 Bump Mythic Dockerfile tag to match release 'v3.3.0.135' 2025-08-27 19:30:33 +00:00
its-a-feature a1094f6076 fixing database global settings 2025-08-27 14:12:17 -05:00
its-a-feature f898a09c83 updating to call on start for logging/webhook containers 2025-08-27 13:55:53 -05:00
github-actions c416292714 Bump Mythic Dockerfile tag to match release 'v3.3.0.133' 2025-08-24 14:14:31 +00:00
its-a-feature 759ab426b6 fixing bug with eventing 2025-08-24 08:57:21 -05:00
its-a-feature cd3e5ed774 updating new table context menu in browser scripts 2025-08-21 10:47:10 -05:00
github-actions ffafcd86b9 Bump Mythic Dockerfile tag to match release 'v3.3.0.131' 2025-08-21 15:04:24 +00:00
its-a-feature 7e9394e070 updating to not pull from env config 2025-08-21 09:42:30 -05:00
its-a-feature 2a6bd6e50b updated compiled ui 2025-08-21 09:34:31 -05:00
its-a-feature c0f7b94b2f browser script options, invite links, global settings, and more 2025-08-21 09:24:55 -05:00
github-actions 62c1151c47 Bump Mythic Dockerfile tag to match release 'v3.3.0.130' 2025-08-20 19:10:46 +00:00
its-a-feature 8deb1d9913 updating pypi package 2025-08-20 13:52:59 -05:00
github-actions 084ab87593 Bump Mythic Dockerfile tag to match release 'v3.3.0.129' 2025-08-19 16:29:51 +00:00
its-a-feature 977f796cb1 updating mythic scripting pypi version 2025-08-19 11:12:17 -05:00
Cody Thomas 07722b7de5 Merge pull request #468 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/multi-96c788614a
Bump on-headers and compression in /MythicReactUI
2025-08-12 09:22:38 -05:00
Cody Thomas b5cac5d3f1 Merge pull request #470 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/form-data-3.0.4
Bump form-data from 3.0.3 to 3.0.4 in /MythicReactUI
2025-08-12 09:22:20 -05:00
github-actions da84bf5a47 Bump Mythic Dockerfile tag to match release 'v3.3.0.128' 2025-08-11 19:39:42 +00:00
its-a-feature ab7f539ed2 fixing bug in eventing creation through UI 2025-08-11 14:22:11 -05:00
its-a-feature ae2d6e8dbd fixing UI bug for eventing task creation 2025-08-04 09:40:39 -05:00
its-a-feature 1b7fd846a2 fixed bug with preferences getting cached 2025-08-01 11:08:15 -05:00
its-a-feature 1e34aeaacc updating slack link 2025-07-30 19:12:06 -05:00
its-a-feature 8e9ca5b14a updating task search to include display params 2025-07-30 12:53:52 -05:00
github-actions 5338775d02 Bump Mythic Dockerfile tag to match release 'v3.3.0.127' 2025-07-30 17:53:08 +00:00
its-a-feature 81c260d4da fixing eventing cron bug 2025-07-30 12:35:26 -05:00
its-a-feature 62f3c4c9c6 adding option to search callbacks by pid 2025-07-29 11:18:16 -05:00
its-a-feature ee7441c599 fixing UI bug with testing consuming services 2025-07-28 18:15:47 -05:00
github-actions 3363e3d2f2 Bump Mythic Dockerfile tag to match release 'v3.3.0.126' 2025-07-25 21:54:58 +00:00
its-a-feature 765018ab74 fixing a UI bug where background images would re-tile on change 2025-07-25 16:37:03 -05:00
dependabot[bot] d36774e0b8 Bump form-data from 3.0.3 to 3.0.4 in /MythicReactUI
Bumps [form-data](https://github.com/form-data/form-data) from 3.0.3 to 3.0.4.
- [Release notes](https://github.com/form-data/form-data/releases)
- [Changelog](https://github.com/form-data/form-data/blob/v3.0.4/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v3.0.3...v3.0.4)

---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 3.0.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-07-22 07:14:52 +00:00
github-actions 56bcba9b8d Bump Mythic Dockerfile tag to match release 'v3.3.0.125' 2025-07-21 16:44:57 +00:00
its-a-feature ae8f98cf0d updating some syncing code to reduce number of syncs 2025-07-21 11:27:03 -05:00
github-actions 09871f8ba3 Bump Mythic Dockerfile tag to match release 'v3.3.0.124' 2025-07-18 15:49:26 +00:00
its-a-feature 02db11e522 tweaking file uploads to more consistently force completion state 2025-07-18 10:31:57 -05:00
dependabot[bot] db090b63ce Bump on-headers and compression in /MythicReactUI
---
updated-dependencies:
- dependency-name: on-headers
  dependency-version: 1.1.0
  dependency-type: indirect
- dependency-name: compression
  dependency-version: 1.8.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-07-17 21:29:45 +00:00
github-actions 4769c29ea1 Bump Mythic Dockerfile tag to match release 'v3.3.0.123' 2025-07-17 16:02:16 +00:00
its-a-feature a6cb5064c5 fixing UI search bug and updating command augment sync states 2025-07-17 10:44:39 -05:00
its-a-feature a725e36443 Updating tag search page and mythic-cli help output 2025-07-16 17:05:00 -05:00
its-a-feature 0ffbe0f0a5 adding more options for tasking context tabs 2025-07-13 08:21:47 -05:00
its-a-feature dc146b8988 updating action menu button color in tables 2025-07-11 13:39:11 -05:00
its-a-feature 77166f400b adding cursor position to ace text fields 2025-07-11 10:34:40 -05:00
github-actions 7dd02aedb4 Bump Mythic Dockerfile tag to match release 'v3.3.0.122' 2025-07-07 16:07:19 +00:00
its-a-feature 6fbf73ceb7 updating subprocess completion flows to preserve subtask status 2025-07-07 10:49:31 -05:00
github-actions a13b8c88c4 Bump Mythic Dockerfile tag to match release 'v3.3.0.121' 2025-07-03 16:21:32 +00:00
its-a-feature b837684a79 updating UI for credential access and server for de-dupe updates 2025-07-03 11:03:51 -05:00
github-actions de2d6080e9 Bump Mythic Dockerfile tag to match release 'v3.3.0.120' 2025-07-02 18:44:29 +00:00
its-a-feature 775156efdc updating cookies for http only Mythic deployments 2025-07-02 13:27:10 -05:00
github-actions 5d5e40369b Bump Mythic Dockerfile tag to match release 'v3.3.0.119' 2025-07-02 04:25:05 +00:00
its-a-feature ac353c3cca adding fallbacks for invite link generation 2025-07-01 23:07:58 -05:00
github-actions a4d61b5e0e Bump Mythic Dockerfile tag to match release 'v3.3.0.118' 2025-07-01 21:22:13 +00:00
its-a-feature 2b4eb73bd5 fixing build bug 2025-07-01 16:04:48 -05:00
its-a-feature b527bf7e0c updating cookie parsing 2025-07-01 07:54:04 -05:00
its-a-feature 4e8eecfe25 fixing email setting bug 2025-06-30 16:39:57 -05:00
its-a-feature faf32c60f1 fixed bug in updating user emails 2025-06-30 15:18:19 -05:00
github-actions 0fcd35a039 Bump Mythic Dockerfile tag to match release 'v3.3.0.117' 2025-06-29 15:05:53 +00:00
its-a-feature 367f954050 adding new task filter options when searching 2025-06-29 09:41:00 -05:00
github-actions 49101478cb Bump Mythic Dockerfile tag to match release 'v3.3.0.116' 2025-06-27 16:49:12 +00:00
its-a-feature 68630fe297 updating PushC2 logic for multiple connections per callback 2025-06-27 12:31:42 -04:00
its-a-feature dbdd60c6fb Adding new payload comparison functionality 2025-06-25 23:03:45 -04:00
its-a-feature c841189f00 updating console view with pr and single task view 2025-06-24 12:23:29 -05:00
Cody Thomas 714aec37cc Merge pull request #464 from DrorDvash/master
New prompt for console-view
2025-06-24 11:49:04 -05:00
its-a-feature 121695b5f1 updating migration constraints 2025-06-23 12:18:34 -05:00
github-actions fd64fa2ba4 Bump Mythic Dockerfile tag to match release 'v3.3.0.115' 2025-06-23 16:39:04 +00:00
its-a-feature bef32bc01b adding tags to payloads/callbacks 2025-06-23 11:21:09 -05:00
Dror Dvash f75cb4f853 New prompt for console-view
2-line prompt with icons.
2025-06-21 18:42:52 +03:00
github-actions f8e71c493b Bump Mythic Dockerfile tag to match release 'v3.3.0.114' 2025-06-20 17:00:38 +00:00
its-a-feature 471d74de44 updated processing of command locations with changing command names 2025-06-20 11:43:33 -05:00
its-a-feature 8cdcd3168b updating tasking UI to show parameter descriptions inline vs hover 2025-06-20 10:12:02 -05:00
github-actions 677ccdcc14 Bump Mythic Dockerfile tag to match release 'v3.3.0.113' 2025-06-18 19:38:11 +00:00
its-a-feature 23e857b940 new ui color choices, checkin bug fix, new edit & build option 2025-06-18 14:20:43 -05:00
github-actions 612eca9061 Bump Mythic Dockerfile tag to match release 'v3.3.0.112' 2025-06-17 18:16:21 +00:00
its-a-feature c16b4d0db3 UI tweaks for cwd and impersonation_context tracking 2025-06-17 12:58:45 -05:00
github-actions d6f454374a Bump Mythic Dockerfile tag to match release 'v3.3.0.111' 2025-06-15 14:59:28 +00:00
its-a-feature 4649197c17 updating some UI color pieces 2025-06-15 09:42:02 -05:00
its-a-feature c3d2bdc328 adding core files for ui 2025-06-13 13:52:39 -05:00
its-a-feature 8f980dcf51 adjusting the tiling for background images 2025-06-13 13:51:26 -05:00
github-actions a21deb9d41 Bump Mythic Dockerfile tag to match release 'v3.3.0.110' 2025-06-13 00:03:16 +00:00
its-a-feature f6535dd91e bug fixes and new cwd/impersonation_context support 2025-06-12 18:45:59 -05:00
its-a-feature b62ea98c10 more UI safeguards for bad ui config values 2025-06-10 14:51:07 -05:00
github-actions 301efc044e Bump Mythic Dockerfile tag to match release 'v3.3.0.109' 2025-06-09 22:37:41 +00:00
its-a-feature 5d0b3d1a94 better handling of dependencies in command attributes 2025-06-09 17:19:19 -05:00
github-actions 25799523d0 Bump Mythic Dockerfile tag to match release 'v3.3.0.108' 2025-06-06 16:16:15 +00:00
its-a-feature 83ef0f2b3a hasura and ui tweaks 2025-06-06 10:58:33 -05:00
github-actions dba92abbc1 Bump Mythic Dockerfile tag to match release 'v3.3.0.107' 2025-06-04 22:18:26 +00:00
its-a-feature ec51eb4f90 new callback blocking functionality and artifact creation workflows 2025-06-04 16:01:15 -05:00
github-actions fcc8398d55 Bump Mythic Dockerfile tag to match release 'v3.3.0.106' 2025-06-03 23:50:23 +00:00
its-a-feature ba75470a64 update database to match new migration version 2025-06-03 18:31:25 -05:00
its-a-feature caa23d3927 fixing UI bug and adding update for mythicrpc call 2025-06-03 18:27:20 -05:00
its-a-feature 7e4603ddec fix for ui auto column width adjustments 2025-06-02 10:26:18 -05:00
its-a-feature 5004a5a043 fixing spacing issue on space payloads table 2025-05-30 16:40:05 -05:00
github-actions e67b1c7339 Bump Mythic Dockerfile tag to match release 'v3.3.0.105' 2025-05-30 19:44:09 +00:00
its-a-feature 6a4c154d75 updating mythic scripting 2025-05-30 11:16:36 -05:00
github-actions d717a58082 Bump Mythic Dockerfile tag to match release 'v3.3.0.104' 2025-05-30 15:06:01 +00:00
its-a-feature 982d7e1391 bulk deletes, ui tweaks, more preferences 2025-05-30 09:48:10 -05:00
github-actions 8b788ccc8c Bump Mythic Dockerfile tag to match release 'v3.3.0.103' 2025-05-22 21:32:54 +00:00
its-a-feature bb280887c6 updating logic bug in callback_token remove processing 2025-05-22 16:15:16 -05:00
github-actions 7d402c4245 Bump Mythic Dockerfile tag to match release 'v3.3.0.102' 2025-05-22 16:46:34 +00:00
its-a-feature 0ddfa50ce4 updating mythic-cli version 2025-05-22 11:28:54 -05:00
Cody Thomas a694353648 Merge pull request #460 from EvanMcBroom/patch-1
Added nginx info to containers to support mythic scripting during container startup
2025-05-22 11:21:58 -05:00
Evan McBroom fbab9d91f6 Reverted editing the wrong array 2025-05-22 12:21:12 -04:00
Evan McBroom 0c95773d21 Added NGINX_USE_SSL 2025-05-22 12:18:29 -04:00
Evan McBroom 1168f79794 Update serviceMetadata.go 2025-05-22 12:13:48 -04:00
github-actions 6188c0fd1e Bump Mythic Dockerfile tag to match release 'v3.3.0.101' 2025-05-21 20:17:54 +00:00
its-a-feature ba1d98dae6 fixing automatic eventing import logic for auto delete 2025-05-21 14:59:50 -05:00
github-actions 8e5b89f06c Bump Mythic Dockerfile tag to match release 'v3.3.0.100' 2025-05-21 18:51:48 +00:00
its-a-feature 5927919dc4 updating server version location 2025-05-21 13:29:39 -05:00
github-actions 7c7a545e86 Bump Mythic Dockerfile tag to match release 'v3.3.0.99' 2025-05-21 14:56:36 +00:00
its-a-feature d1443be757 fixing some UI issues with updated apollo query library for searching 2025-05-21 09:37:52 -05:00
github-actions ac49ee1d89 Bump Mythic Dockerfile tag to match release 'v3.3.0.98' 2025-05-19 22:09:33 +00:00
its-a-feature 463167427f fixed looping issue with process data pid reuse 2025-05-19 16:51:50 -05:00
github-actions 57085f500b Bump Mythic Dockerfile tag to match release 'v3.3.0.97' 2025-05-16 20:21:36 +00:00
its-a-feature 8365c9e0bf fixing issue with toggling apitoken hidden 2025-05-16 15:04:03 -05:00
github-actions 31115f18fc Bump Mythic Dockerfile tag to match release 'v3.3.0.96' 2025-05-16 20:02:36 +00:00
its-a-feature 83cf83edea fixing settings table getter function 2025-05-16 14:45:00 -05:00
github-actions b800a1dcb0 Bump Mythic Dockerfile tag to match release 'v3.3.0.95' 2025-05-16 19:02:10 +00:00
its-a-feature 846a181bb4 fixing a few UI bugs for bulk actions 2025-05-16 13:43:24 -05:00
github-actions a0517b6a52 Bump Mythic Dockerfile tag to match release 'v3.3.0.94' 2025-05-14 15:02:08 +00:00
its-a-feature c47d16f0d0 fixing bug with updated payload command lists 2025-05-14 09:43:25 -05:00
github-actions 208d580d96 Bump Mythic Dockerfile tag to match release 'v3.3.0.93' 2025-05-11 14:03:13 +00:00
its-a-feature 8ead31a150 UI updates, server fixes 2025-05-11 08:46:17 -05:00
Cody Thomas fef2234bed Merge pull request #458 from piolug93/patch-1
Add missing field in sql statment in MythicRPCAPITokenCreate
2025-05-08 11:32:01 -05:00
piolug93 9299cf9ef9 Add missing field in sql statment in MythicRPCAPITokenCreate 2025-05-08 17:48:20 +02:00
its-a-feature 26decb421f updated virtualized row heights based on text font size 2025-04-16 13:01:34 -05:00
its-a-feature 4044c1346c updating tag search fields to distinct on tagged item id not tag id 2025-04-15 13:59:19 -05:00
github-actions 0c887f4acf Bump Mythic Dockerfile tag to match release 'v3.3.0.92' 2025-04-15 14:43:27 +00:00
its-a-feature 81934665ef reduce duplicate c2 starts and update log for new payloads 2025-04-15 09:15:51 -05:00
github-actions 5c48a51507 Bump Mythic Dockerfile tag to match release 'v3.3.0.91' 2025-04-10 21:51:14 +00:00
its-a-feature b295ebd3ae updating the handling of report generation 2025-04-10 16:33:07 -05:00
its-a-feature ab699c5685 slight UI tweak for text fields 2025-04-10 15:46:18 -05:00
github-actions dd9bef6daf Bump Mythic Dockerfile tag to match release 'v3.3.0.90' 2025-04-09 18:28:39 +00:00
its-a-feature 19bb380f12 updated linked agent checkins and updated mythictree task ids 2025-04-09 13:09:41 -05:00
its-a-feature 5aa4d3453f fixing label 2025-04-07 11:43:34 -05:00
github-actions 59862978de Bump Mythic Dockerfile tag to match release 'v3.3.0.89' 2025-04-07 15:59:09 +00:00
its-a-feature 48e1ddb9a5 update socks to bind to ipv4 addresses 2025-04-07 10:41:37 -05:00
its-a-feature 9d92b1644b slight UI fix 2025-04-07 10:15:33 -05:00
github-actions 04992aa1bc Bump Mythic Dockerfile tag to match release 'v3.3.0.88' 2025-04-04 13:24:08 +00:00
its-a-feature 98eced6f79 fixing sql typo 2025-04-04 08:06:23 -05:00
github-actions ef6fb08cba Bump Mythic Dockerfile tag to match release 'v3.3.0.87' 2025-04-04 02:26:57 +00:00
its-a-feature bc9f3287b7 fixing tag type get or create rpc call 2025-04-03 21:09:38 -05:00
github-actions 16fdecee28 Bump Mythic Dockerfile tag to match release 'v3.3.0.86' 2025-04-02 19:29:42 +00:00
its-a-feature 4fad02e3c9 fix for tag creation 2025-04-02 14:12:06 -05:00
github-actions d3d6d4a2fb Bump Mythic Dockerfile tag to match release 'v3.3.0.85' 2025-04-01 19:07:07 +00:00
its-a-feature ce586d6980 few UI tweaks 2025-04-01 13:48:45 -05:00
github-actions 69feb548e1 Bump Mythic Dockerfile tag to match release 'v3.3.0.84' 2025-03-28 18:55:18 +00:00
its-a-feature 9290ca16af UI tweaks, new eventing trigger, interactive updates, new RPC calls 2025-03-28 13:22:36 -05:00
github-actions 30e5c69dc4 Bump Mythic Dockerfile tag to match release 'v3.3.0.83' 2025-03-19 19:51:04 +00:00
its-a-feature ef7f39f19d fixing UI updating of deleted files 2025-03-19 14:04:39 -05:00
github-actions 6361d93672 Bump Mythic Dockerfile tag to match release 'v3.3.0.82' 2025-03-14 15:00:48 +00:00
its-a-feature 855f633e74 Updating response intercept to not trigger on empty responses 2025-03-14 09:42:51 -05:00
github-actions 26afca9280 Bump Mythic Dockerfile tag to match release 'v3.3.0.81' 2025-03-13 21:03:28 +00:00
its-a-feature a1d225612e updating readme 2025-03-13 15:35:37 -05:00
its-a-feature 80ecc5cfe2 fixing bad struct tag in eventing conditional checks for pypi 2025-03-13 15:26:28 -05:00
its-a-feature b3dcc7d1fc fixing UI vertical icons on short screens 2025-03-12 13:30:20 -05:00
Cody Thomas 166869f6c5 Merge pull request #439 from pidone/fix_install_scripts
Fix Docker installation scripts
2025-03-12 12:36:23 -05:00
github-actions 520995fab9 Bump Mythic Dockerfile tag to match release 'v3.3.0.79' 2025-03-12 16:22:46 +00:00
its-a-feature 5878378db7 updating rabbitmq retry loops 2025-03-12 11:04:10 -05:00
github-actions a819b51404 Bump Mythic Dockerfile tag to match release 'v3.3.0.78' 2025-03-11 22:23:16 +00:00
its-a-feature a906c1afcf allowing custom status during create_tasking 2025-03-11 16:56:27 -05:00
github-actions b72ec32a92 Bump Mythic Dockerfile tag to match release 'v3.3.0.77' 2025-03-07 18:49:48 +00:00
its-a-feature e68be3891a UI tweaks and timeskew updates, server fixes for automation and search 2025-03-07 12:26:59 -06:00
github-actions 340ecb1625 Bump Mythic Dockerfile tag to match release 'v3.3.0.76' 2025-03-05 21:47:25 +00:00
its-a-feature 37fcbd0728 updating file size to bigint to track larger files properly 2025-03-05 15:30:04 -06:00
github-actions ef4637e5ed Bump Mythic Dockerfile tag to match release 'v3.3.0.75' 2025-03-05 20:15:04 +00:00
its-a-feature bbfcb19366 updating rabbitmq looping to add more retries 2025-03-05 13:56:57 -06:00
github-actions 86c6488a89 Bump Mythic Dockerfile tag to match release 'v3.3.0.74' 2025-03-04 23:56:27 +00:00
its-a-feature 1366ebed6c mythic-cli agent install updates and slight ui tweak 2025-03-04 17:24:59 -06:00
github-actions 68c2005ff3 Bump Mythic Dockerfile tag to match release 'v3.3.0.73' 2025-02-28 20:31:53 +00:00
its-a-feature 5f9c16099e adding stdout/stderr from tasks and ui fixes 2025-02-28 13:53:13 -06:00
its-a-feature 71ca8d9122 reduced debouncing on some fields and notification fix 2025-02-21 14:30:32 -06:00
github-actions 2fb43a3314 Bump Mythic Dockerfile tag to match release 'v3.3.0.72' 2025-02-21 16:58:28 +00:00
its-a-feature 0831e8c4e5 added UI config setting for timestamps and rabbitmq server updates 2025-02-21 10:33:33 -06:00
github-actions 9d2e4524eb Bump Mythic Dockerfile tag to match release 'v3.3.0.71' 2025-02-20 20:41:41 +00:00
its-a-feature 1390b4ead6 ui tweak and rabbitmq update for retries 2025-02-20 14:03:11 -06:00
github-actions 734eaba613 Bump Mythic Dockerfile tag to match release 'v3.3.0.70' 2025-02-19 01:43:52 +00:00
its-a-feature 26c64c9763 bug fix for file create mythic rpc with payload uuid 2025-02-18 19:26:35 -06:00
its-a-feature 86fb7ebcaa removing trailing comma 2025-02-18 19:12:39 -06:00
github-actions b2eebeea4e Bump Mythic Dockerfile tag to match release 'v3.3.0.69' 2025-02-18 16:31:06 +00:00
its-a-feature 794b947c1e updating mythic-cli for mythic_sync 2025-02-18 10:13:11 -06:00
github-actions ec58b1293d Bump Mythic Dockerfile tag to match release 'v3.3.0.68' 2025-02-14 17:15:47 +00:00
its-a-feature 695948aac9 bug fix when downloading 0byte file 2025-02-14 10:58:11 -06:00
its-a-feature 9741c7a66b slight ui tweak for handling newlines in array entries in modals 2025-02-13 15:59:41 -06:00
github-actions f363544542 Bump Mythic Dockerfile tag to match release 'v3.3.0.67' 2025-02-13 18:12:38 +00:00
its-a-feature 2f0c7d7c54 mythic-cli fix for mythic_sync installs 2025-02-13 11:54:48 -06:00
its-a-feature dddca779c6 updating sponsors 2025-02-13 08:53:58 -06:00
github-actions 6fd06f8f5c Bump Mythic Dockerfile tag to match release 'v3.3.0.66' 2025-02-10 15:44:08 +00:00
its-a-feature a694d3dd20 rabbitmq reverts 2025-02-10 09:28:17 -06:00
github-actions 1f27126215 Bump Mythic Dockerfile tag to match release 'v3.3.0.65' 2025-02-10 15:24:46 +00:00
its-a-feature 343cefcfec rabbitmq queue tweaks 2025-02-10 09:08:39 -06:00
github-actions fa245e66e9 Bump Mythic Dockerfile tag to match release 'v3.3.0.64' 2025-02-07 23:36:20 +00:00
its-a-feature 1559d5b924 few UI fixes and backend tweaks 2025-02-07 17:21:03 -06:00
its-a-feature 7be2931d09 ui fix for interactive tasking 2025-02-05 17:52:41 -06:00
its-a-feature 83aa363aa0 fixing bug with interactive tasking UI 2025-02-04 12:49:24 -06:00
its-a-feature f5f34448be improving performance on text fields with debouncing 2025-01-31 14:25:30 -06:00
its-a-feature 14986933d6 few UI tweaks 2025-01-30 15:53:12 -06:00
github-actions 8e35c4d922 Bump Mythic Dockerfile tag to match release 'v3.3.0.63' 2025-01-30 18:33:50 +00:00
its-a-feature d6608d687c bug fix for command augment containers 2025-01-30 12:18:41 -06:00
github-actions 436641bb07 Bump Mythic Dockerfile tag to match release 'v3.3.0.62' 2025-01-30 15:53:19 +00:00
its-a-feature 554128d8e3 few bug fixes in server and UI 2025-01-30 09:37:02 -06:00
Pidone 3c934d9758 Fix Docker installation scripts
apt-key is deprecated and already removed on kali.
2025-01-29 16:52:14 +01:00
its-a-feature 1e17d90c5d making a few components have transparent backgrounds 2025-01-24 16:01:17 -06:00
github-actions fe3e788b89 Bump Mythic Dockerfile tag to match release 'v3.3.0.61' 2025-01-24 20:42:09 +00:00
its-a-feature 2772b6c520 mythicRPC fixes and a few UI tweaks 2025-01-24 14:26:23 -06:00
its-a-feature 90ec9e2496 updating dependencies 2025-01-18 12:05:33 -06:00
its-a-feature 78eb2edba3 slight UI tweak for tag search page 2025-01-18 11:57:49 -06:00
github-actions a88a1f79d4 Bump Mythic Dockerfile tag to match release 'v3.3.0.60' 2025-01-18 17:51:45 +00:00
its-a-feature 1255a2560e many GUI enhancements and a few API additions for eventing 2025-01-18 11:36:21 -06:00
its-a-feature 156bca4692 adjusting dashboard page 2024-12-31 17:24:06 -06:00
its-a-feature e22274760c few more ui tweaks 2024-12-31 15:54:15 -06:00
github-actions 6ded9fa3f7 Bump Mythic Dockerfile tag to match release 'v3.3.0.59' 2024-12-31 19:31:00 +00:00
its-a-feature 191191e538 ui tweaks, header size changes, icon fixing 2024-12-31 13:15:49 -06:00
github-actions 8d63fadb2a Bump Mythic Dockerfile tag to match release 'v3.3.0.58' 2024-12-31 17:58:46 +00:00
its-a-feature 2a43f18c20 updating new vertical nav bar 2024-12-31 11:43:24 -06:00
github-actions 20779914d4 Bump Mythic Dockerfile tag to match release 'v3.3.0.57' 2024-12-28 16:18:53 +00:00
its-a-feature fa4b57dfd8 adding new UI option for vertical/customizable nav bar 2024-12-28 09:46:26 -06:00
github-actions 90128d0b28 Bump Mythic Dockerfile tag to match release 'v3.3.0.56' 2024-12-25 15:38:20 +00:00
its-a-feature 81a19552d7 few UI fixes 2024-12-25 09:22:47 -06:00
github-actions 801a2dad26 Bump Mythic Dockerfile tag to match release 'v3.3.0.55' 2024-12-23 17:56:26 +00:00
its-a-feature 5efdde0944 server skew, callback colors, bug fixes, and prep for future updates 2024-12-23 11:41:06 -06:00
its-a-feature 2a8b5d2711 fix for task status with interactive tasking 2024-12-18 19:39:10 -06:00
github-actions e43295e2dd Bump Mythic Dockerfile tag to match release 'v3.3.0.54' 2024-12-18 21:50:35 +00:00
its-a-feature 0e942f09bc fix issue with missing containers 2024-12-18 15:35:04 -06:00
github-actions 062504298b Bump Mythic Dockerfile tag to match release 'v3.3.0.53' 2024-12-18 18:56:01 +00:00
its-a-feature d30efca930 updating mythic cli to speed things up a bit 2024-12-18 11:34:09 -06:00
github-actions 82b67d65e0 Bump Mythic Dockerfile tag to match release 'v3.3.0.52' 2024-12-18 14:39:01 +00:00
its-a-feature 85efc505d5 login fix 2024-12-18 08:23:03 -06:00
github-actions af15bcb113 Bump Mythic Dockerfile tag to match release 'v3.3.0.51' 2024-12-17 21:24:11 +00:00
its-a-feature f4306629db Merge remote-tracking branch 'origin/master' 2024-12-17 15:08:24 -06:00
its-a-feature 173cd294e9 slight UI update for task response streaming and time syncing 2024-12-17 15:08:15 -06:00
github-actions 076d8f762f Bump Mythic Dockerfile tag to match release 'v3.3.0.50' 2024-12-15 14:09:21 +00:00
its-a-feature eddbd55141 updating MythicRPC commands for Callback Search/Add/Remove 2024-12-14 10:19:22 -06:00
github-actions 2991f5339e Bump Mythic Dockerfile tag to match release 'v3.3.0.49' 2024-12-13 16:08:27 +00:00
its-a-feature e5b7dbfe8e updating payload syncing flags 2024-12-13 09:52:57 -06:00
github-actions 1630f03209 Bump Mythic Dockerfile tag to match release 'v3.3.0.48' 2024-12-13 00:25:56 +00:00
its-a-feature 9cdd6c669c UI and Server fixes for dynamic command augmentation 2024-12-12 18:09:48 -06:00
github-actions 3c87e7be9e Bump Mythic Dockerfile tag to match release 'v3.3.0.47' 2024-12-12 20:59:13 +00:00
its-a-feature 4b704f5d99 UI fixes 2024-12-12 14:43:57 -06:00
github-actions 31a0f3b642 Bump Mythic Dockerfile tag to match release 'v3.3.0.46' 2024-12-10 21:47:56 +00:00
Cody Thomas 554a135370 Merge pull request #433 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/nanoid-3.3.8
Bump nanoid from 3.3.7 to 3.3.8 in /MythicReactUI
2024-12-10 13:53:21 -06:00
dependabot[bot] 9f79f4d7fe Bump nanoid from 3.3.7 to 3.3.8 in /MythicReactUI
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.7 to 3.3.8.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2024-12-10 19:07:54 +00:00
its-a-feature 64a08d92ff adding optimizations for messages with many responses
also added a few UI fixes
2024-12-10 13:06:43 -06:00
github-actions 42b62058a3 Bump Mythic Dockerfile tag to match release 'v3.3.0.45' 2024-12-04 18:15:40 +00:00
its-a-feature 2d35ea1eec UI update to fix callback tokens rendering issue 2024-12-04 07:58:05 -10:00
github-actions 331b95805d Bump Mythic Dockerfile tag to match release 'v3.3.0.44' 2024-11-27 22:22:18 +00:00
its-a-feature b4aa736586 updating the _use_volume setting to false for new agent installs 2024-11-27 16:07:07 -06:00
github-actions ec2078d339 Bump Mythic Dockerfile tag to match release 'v3.3.0.43' 2024-11-27 20:59:29 +00:00
its-a-feature 16b6d33526 updated to not raise errors unnecessarily for c2 status updates 2024-11-27 14:43:57 -06:00
its-a-feature 96b05c9333 few UI tweaks on the payloads page for better output viewing 2024-11-27 14:23:28 -06:00
github-actions 83f81c9dca Bump Mythic Dockerfile tag to match release 'v3.3.0.42' 2024-11-26 20:31:47 +00:00
its-a-feature ab5c5a9768 updating actions for invite links 2024-11-26 14:16:50 -06:00
its-a-feature 56ef65276c updating command augment injection args 2024-11-26 14:09:44 -06:00
github-actions 467d948ac1 Bump Mythic Dockerfile tag to match release 'v3.3.0.41' 2024-11-26 19:48:02 +00:00
its-a-feature 82b335cded updates to invite links 2024-11-26 13:18:34 -06:00
github-actions f9417645d2 Bump Mythic Dockerfile tag to match release 'v3.3.0.40' 2024-11-22 16:45:17 +00:00
its-a-feature c8d6e9fa9e adding new features and some slight ui fixes 2024-11-22 10:23:14 -06:00
github-actions 8f7625c883 Bump Mythic Dockerfile tag to match release 'v3.3.0.39' 2024-11-18 21:06:45 +00:00
its-a-feature 2752e5fece updating file hosting processes 2024-11-18 14:51:53 -06:00
github-actions 8a96739120 Bump Mythic Dockerfile tag to match release 'v3.3.0.38' 2024-11-18 18:20:18 +00:00
its-a-feature 99f1e7a7a3 onStart container apitoken fix and slight UI tweak 2024-11-18 12:04:28 -06:00
its-a-feature 688b2ca946 re-adding non-git lfs files 2024-11-15 15:09:37 -06:00
its-a-feature 71ba45c2f9 removing .gitattributes on this branch 2024-11-15 15:06:03 -06:00
its-a-feature b2443d1ca2 updating react files to use git lfs 2024-11-15 14:25:07 -06:00
its-a-feature ade07381c1 updating gitattributes 2024-11-15 14:21:49 -06:00
its-a-feature 228c0ab57b updating git attributes for lfs on compiled react ui pages 2024-11-15 14:17:25 -06:00
github-actions 43a081c82d Bump Mythic Dockerfile tag to match release 'v3.3.0.37' 2024-11-15 03:09:54 +00:00
its-a-feature c51b900777 small ui tweak 2024-11-14 20:52:43 -06:00
github-actions 0cfb6b0ca7 Bump Mythic Dockerfile tag to match release 'v3.3.0.36' 2024-11-15 02:39:50 +00:00
its-a-feature 51cf1c9ff5 Save user preferences to database 2024-11-14 20:24:22 -06:00
github-actions 05ed8b14f1 Bump Mythic Dockerfile tag to match release 'v3.3.0.35' 2024-11-08 21:32:52 +00:00
its-a-feature 55c95cba5b slight ui fix 2024-11-08 15:17:17 -06:00
github-actions 5fae8eeb7d Bump Mythic Dockerfile tag to match release 'v3.3.0.34' 2024-11-06 21:16:37 +00:00
its-a-feature 8da0ed6ace one more tweak to the payload search 2024-11-06 15:01:29 -06:00
its-a-feature ab63e3862e updating to fix payload search bug 2024-11-06 14:56:48 -06:00
github-actions 6683e9853f Bump Mythic Dockerfile tag to match release 'v3.3.0.32' 2024-11-06 13:58:11 +00:00
its-a-feature fe59952384 few UI CLI tweaks 2024-11-06 07:42:43 -06:00
github-actions 96fe2e8a49 Bump Mythic Dockerfile tag to match release 'v3.3.0.31' 2024-11-05 21:08:33 +00:00
its-a-feature badb562ebe adding additional checks for operator status 2024-11-05 14:52:52 -06:00
its-a-feature 4d95dc8d36 fixing opsec check bypass request 2024-11-05 14:48:07 -06:00
github-actions 6b2de0c719 Bump Mythic Dockerfile tag to match release 'v3.3.0.29' 2024-11-05 17:36:48 +00:00
its-a-feature e1c37ba5ca UI updates and temp tweak for wrappers 2024-11-05 11:21:42 -06:00
github-actions ef79c90d75 Bump Mythic Dockerfile tag to match release 'v3.3.0.28' 2024-11-01 12:50:18 +00:00
its-a-feature 319a71598a fixing regression with screenshots added to file browser 2024-11-01 08:34:51 -04:00
github-actions ce5057e58d Bump Mythic Dockerfile tag to match release 'v3.3.0.27' 2024-10-28 18:47:21 +00:00
its-a-feature e8737b4223 updating missing hasura permissions 2024-10-28 13:32:15 -05:00
github-actions 767f82aa3d Bump Mythic Dockerfile tag to match release 'v3.3.0.26' 2024-10-28 17:16:00 +00:00
its-a-feature 5f85d9e716 fixing bug with processes not getting marked as deleted 2024-10-28 12:00:57 -05:00
github-actions 74536d4948 Bump Mythic Dockerfile tag to match release 'v3.3.0.25' 2024-10-28 14:44:20 +00:00
its-a-feature 5979d236bc ui fixes and proxy stop fix via MythicRPC
added support for setting cli history to be display params vs original params per agent
2024-10-28 09:13:56 -05:00
github-actions 641c8e1d60 Bump Mythic Dockerfile tag to match release 'v3.3.0.24' 2024-10-18 13:45:36 +00:00
its-a-feature 208611543c Updating to better reflected deleted payloads not making callbacks 2024-10-18 08:30:46 -05:00
github-actions dfad0dea3d Bump Mythic Dockerfile tag to match release 'v3.3.0.23' 2024-10-16 17:02:26 +00:00
its-a-feature f7cd34d9da UI update to save callbacks table column widths 2024-10-16 11:36:38 -05:00
github-actions db37b43465 Bump Mythic Dockerfile tag to match release 'v3.3.0.22' 2024-10-14 17:16:26 +00:00
its-a-feature 617d927472 Many UI Fixes and features added
added UDP Associate and more UI Tab-complete options
2024-10-14 11:25:33 -05:00
Cody Thomas 63a6d5e106 Merge pull request #411 from art-dsit/art/rootless_2
Prevent root-owned files ending up on the filesystem during CLI build
2024-10-08 08:20:48 -05:00
github-actions e58eead1fd Bump Mythic Dockerfile tag to match release 'v3.3.0.21' 2024-10-01 23:07:08 +00:00
Cody Thomas 52d33aef7a Merge pull request #409 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/multi-d66d039ac5
Bump serve-static and express in /MythicReactUI
2024-10-01 17:48:56 -05:00
Cody Thomas 2aa45ebb3a Merge pull request #385 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/ejs-3.1.10
Bump ejs from 3.1.9 to 3.1.10 in /MythicReactUI
2024-10-01 17:48:44 -05:00
Cody Thomas eb730689b0 Merge pull request #404 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/webpack-5.94.0
Bump webpack from 5.92.0 to 5.94.0 in /MythicReactUI
2024-10-01 17:48:34 -05:00
Cody Thomas 7369449c49 Merge pull request #410 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/multi-cf87d80143
Bump send and express in /MythicReactUI
2024-10-01 17:48:25 -05:00
Cody Thomas 22550d9022 Merge pull request #412 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/multi-9423f4c335
Bump body-parser and express in /MythicReactUI
2024-10-01 17:48:11 -05:00
Cody Thomas 150209dc8f Merge pull request #413 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/rollup-2.79.2
Bump rollup from 2.79.1 to 2.79.2 in /MythicReactUI
2024-10-01 17:47:29 -05:00
its-a-feature 4702b6a00c removing fade transitions to make a snappier UI 2024-10-01 17:47:00 -05:00
github-actions 122fe892af Bump Mythic Dockerfile tag to match release 'v3.3.0.20' 2024-09-29 16:14:40 +00:00
its-a-feature faaab0d3a1 Allow tab-cycle ChooseOne values on the command line in the UI 2024-09-29 10:59:37 -05:00
github-actions eaccded888 Bump Mythic Dockerfile tag to match release 'v3.3.0.19' 2024-09-27 20:22:29 +00:00
its-a-feature 273f9a5c71 UI QoL and proxy stop fixes 2024-09-27 15:07:30 -05:00
dependabot[bot] 3b58f30f3d Bump rollup from 2.79.1 to 2.79.2 in /MythicReactUI
Bumps [rollup](https://github.com/rollup/rollup) from 2.79.1 to 2.79.2.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.79.1...v2.79.2)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2024-09-27 17:14:20 +00:00
github-actions a4533926a3 Bump Mythic Dockerfile tag to match release 'v3.3.0.18' 2024-09-26 01:41:34 +00:00
its-a-feature c4284e5f10 added some max size checks for postgres inserts 2024-09-25 20:26:29 -05:00
github-actions c2d386f8ec Bump Mythic Dockerfile tag to match release 'v3.3.0.17' 2024-09-24 16:33:20 +00:00
its-a-feature 2c52598d1c Adding a bit more verbose output for agent error messages 2024-09-24 11:18:13 -05:00
github-actions 13abd19cf5 Bump Mythic Dockerfile tag to match release 'v3.3.0.16' 2024-09-23 22:37:50 +00:00
its-a-feature dc3caff0b5 updating missing struct tag 2024-09-23 17:22:20 -05:00
github-actions efa88a329d Bump Mythic Dockerfile tag to match release 'v3.3.0.15' 2024-09-23 17:00:29 +00:00
its-a-feature 605cbf8fc3 fixing UI visual bug and starting to address potential database deadlock 2024-09-23 11:44:52 -05:00
dependabot[bot] 5bc4c0a993 Bump body-parser and express in /MythicReactUI
Bumps [body-parser](https://github.com/expressjs/body-parser) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `body-parser` from 1.20.2 to 1.20.3
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/body-parser/compare/1.20.2...1.20.3)

Updates `express` from 4.19.2 to 4.21.0
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.0)

---
updated-dependencies:
- dependency-name: body-parser
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2024-09-22 01:51:00 +00:00
Art.Cathain 033ce4f6f6 fix 2024-09-18 13:58:44 +00:00
Art.Cathain bbf0324cb1 toor 2024-09-18 13:54:48 +00:00
github-actions e0618a487b Bump Mythic Dockerfile tag to match release 'v3.3.0.14' 2024-09-17 20:00:10 +00:00
its-a-feature 0b539dfafb fixing initializer 2024-09-17 14:44:20 -05:00
github-actions e83bd2e67a Bump Mythic Dockerfile tag to match release 'v3.3.0.13' 2024-09-17 17:39:48 +00:00
its-a-feature f09c3695f4 adjusting database initialization to auto create bot account 2024-09-17 12:24:32 -05:00
dependabot[bot] 3cd5ff5bd7 Bump send and express in /MythicReactUI
Bumps [send](https://github.com/pillarjs/send) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `send` from 0.18.0 to 0.19.0
- [Release notes](https://github.com/pillarjs/send/releases)
- [Changelog](https://github.com/pillarjs/send/blob/master/HISTORY.md)
- [Commits](https://github.com/pillarjs/send/compare/0.18.0...0.19.0)

Updates `express` from 4.19.2 to 4.21.0
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.0)

---
updated-dependencies:
- dependency-name: send
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2024-09-17 10:49:38 +00:00
dependabot[bot] 07fac1c321 Bump serve-static and express in /MythicReactUI
Bumps [serve-static](https://github.com/expressjs/serve-static) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `serve-static` from 1.15.0 to 1.16.2
- [Release notes](https://github.com/expressjs/serve-static/releases)
- [Changelog](https://github.com/expressjs/serve-static/blob/v1.16.2/HISTORY.md)
- [Commits](https://github.com/expressjs/serve-static/compare/v1.15.0...v1.16.2)

Updates `express` from 4.19.2 to 4.21.0
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.0)

---
updated-dependencies:
- dependency-name: serve-static
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2024-09-17 10:23:56 +00:00
github-actions 39fde7b75a Bump Mythic Dockerfile tag to match release 'v3.3.0.12' 2024-09-14 14:47:39 +00:00
its-a-feature 9032a6f70b updating tasking tabs to be more consistent with task handling 2024-09-14 09:32:53 -05:00
github-actions fcb1dd53b1 Bump Mythic Dockerfile tag to match release 'v3.3.0.11' 2024-09-13 21:22:59 +00:00
its-a-feature 9852d5d06c UI bug fix and increased web logging 2024-09-13 16:07:33 -05:00
github-actions 11bfff6736 Bump Mythic Dockerfile tag to match release 'v3.3.0.10' 2024-09-10 20:20:14 +00:00
its-a-feature 1f9581bc65 fixing c2 profile config page skipping keystrokes 2024-09-10 15:04:58 -05:00
github-actions 3c8243ed21 Bump Mythic Dockerfile tag to match release 'v3.3.0.9' 2024-09-09 20:27:43 +00:00
its-a-feature d223507785 few UI tweaks and better error messages 2024-09-09 15:11:56 -05:00
github-actions ed03ae279b Bump Mythic Dockerfile tag to match release 'v3.3.0.8' 2024-09-04 19:37:02 +00:00
dependabot[bot] 59b9003552 Bump webpack from 5.92.0 to 5.94.0 in /MythicReactUI
Bumps [webpack](https://github.com/webpack/webpack) from 5.92.0 to 5.94.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.92.0...v5.94.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2024-09-04 19:23:05 +00:00
its-a-feature c8a1437880 mythic-cli updates, MythicRPC updates, and mythic ui tweaks 2024-09-04 14:21:52 -05:00
github-actions 937fcbdb17 Bump Mythic Dockerfile tag to match release 'v3.3.0.7' 2024-09-03 03:29:05 +00:00
its-a-feature bde3dbfa66 Merge branch 'master' of https://github.com/its-a-feature/Mythic 2024-09-02 22:14:13 -05:00
its-a-feature 3146b8a76b adding first chunk with file download capabilities 2024-09-02 22:14:01 -05:00
github-actions bec5422883 Bump Mythic Dockerfile tag to match release 'v3.3.0.6' 2024-09-02 17:17:47 +00:00
its-a-feature 85f617a0ef fixing file search bug 2024-09-02 12:02:45 -05:00
github-actions c55e41a816 Bump Mythic Dockerfile tag to match release 'v3.3.0.5' 2024-08-30 22:19:34 +00:00
its-a-feature 3c66f77454 file search bug fix, eventing examples, and ui bug fixes 2024-08-30 17:04:37 -05:00
Cody Thomas f82f8fb738 Merge pull request #396 from launchdaemon/master
Make install_docker scripts architecture independent
2024-08-30 09:08:39 -05:00
github-actions 3fb69aa921 Bump Mythic Dockerfile tag to match release 'v3.3.0.4' 2024-08-30 14:08:15 +00:00
its-a-feature 9da13680e5 Merge branch 'master' of https://github.com/its-a-feature/Mythic 2024-08-30 08:53:48 -05:00
its-a-feature 6daf1cf211 Fixing bug with last checkin times when restarting 2024-08-30 08:53:38 -05:00
github-actions f4494f379a Bump Mythic Dockerfile tag to match release 'v3.3.0.3' 2024-08-29 18:42:42 +00:00
its-a-feature fef7336509 file browser and media display tweaks 2024-08-29 13:27:37 -05:00
github-actions 5c17b50ba5 Bump Mythic Dockerfile tag to match release 'v3.3.0.2' 2024-08-28 23:05:00 +00:00
its-a-feature e8f48ce5ff Merge branch 'master' of https://github.com/its-a-feature/Mythic 2024-08-28 17:50:07 -05:00
its-a-feature 5fdf8f11fc file browser bug fix and ui tweak 2024-08-28 17:49:46 -05:00
github-actions cde711ab3e Bump Mythic Dockerfile tag to match release 'v3.3.0.1' 2024-08-28 21:59:20 +00:00
its-a-feature 981414297c Merge branch 'master' of https://github.com/its-a-feature/Mythic 2024-08-28 16:43:41 -05:00
its-a-feature a6b5cd1db0 fixing UI bug on fresh installs with file browser 2024-08-28 16:43:09 -05:00
github-actions 477f7cf125 Bump Mythic Dockerfile tag to match release 'v3.3.0.0' 2024-08-28 18:57:00 +00:00
Cody Thomas 9b9469622f Merge pull request #400 from its-a-feature/Mythic3.3
rolling back docker api change
2024-08-28 13:22:56 -05:00
its-a-feature 346d0dd6d4 rolling back docker api change 2024-08-28 13:22:29 -05:00
Cody Thomas 9ff3015e15 Merge pull request #399 from its-a-feature/Mythic3.3
Mythic3.3
2024-08-28 13:14:24 -05:00
Cody Thomas 3ac8a64cdd Merge branch 'master' into Mythic3.3 2024-08-28 13:10:57 -05:00
its-a-feature 9be2fa335a Merge branch 'Mythic3.3' of https://github.com/its-a-feature/Mythic into Mythic3.3 2024-08-28 13:08:37 -05:00
its-a-feature 89d336e455 Final update for base Mythic3.3 2024-08-28 13:08:22 -05:00
github-actions 9b4b9d8b6b Bump Mythic Dockerfile tag to match release 'v3.2.20' 2024-08-28 15:12:27 +00:00
github-actions e0ae20697b Bump Mythic Dockerfile tag to match release 'v0.0.4.41' 2024-08-27 23:32:07 +00:00
its-a-feature 7b87af9d4d fixing an extra set of quotes 2024-08-27 18:17:28 -05:00
github-actions 5bb16bb39f Bump Mythic Dockerfile tag to match release 'v0.0.4.40' 2024-08-27 23:03:39 +00:00
its-a-feature e64deac9bb Merge branch 'Mythic3.3' of https://github.com/its-a-feature/Mythic into Mythic3.3 2024-08-27 17:48:47 -05:00
its-a-feature bbac3fc253 removing duplicates from extra_hosts 2024-08-27 17:48:35 -05:00
github-actions 48105e7b45 Bump Mythic Dockerfile tag to match release 'v0.0.4.39' 2024-08-27 22:42:44 +00:00
its-a-feature 65e32fc687 adding "host" option for mythic networking and test workflow files 2024-08-27 17:28:15 -05:00
github-actions f82a81dc9a Bump Mythic Dockerfile tag to match release 'v0.0.4.38' 2024-08-27 00:17:58 +00:00
its-a-feature ac76b0088a fixing UI colors in light mode 2024-08-26 19:03:08 -05:00
github-actions f03e694a92 Bump Mythic Dockerfile tag to match release 'v0.0.4.37' 2024-08-24 19:41:42 +00:00
its-a-feature 1e0f1d6865 Merge branch 'Mythic3.3' of https://github.com/its-a-feature/Mythic into Mythic3.3 2024-08-24 14:27:28 -05:00
its-a-feature 3da1dda00f quick fix for delegate messages 2024-08-24 14:26:59 -05:00
github-actions 3a792024eb Bump Mythic Dockerfile tag to match release 'v0.0.4.36' 2024-08-23 21:48:09 +00:00
its-a-feature 596c6f799b Updated processing for delegates and file downloads, more ui improvement 2024-08-23 16:34:03 -05:00
github-actions d3d5e5c913 Bump Mythic Dockerfile tag to match release 'v0.0.4.35' 2024-08-23 13:52:28 +00:00
its-a-feature 86ec7b2757 removing unnecessary linked agent messages 2024-08-23 08:38:31 -05:00
github-actions 7586b27de3 Bump Mythic Dockerfile tag to match release 'v0.0.4.34' 2024-08-21 20:15:32 +00:00
its-a-feature 4bc0e5974e adding new ability to limit credential dropdowns in the ui 2024-08-21 14:59:27 -05:00
github-actions 3be20a22ee Bump Mythic Dockerfile tag to match release 'v0.0.4.33' 2024-08-20 21:17:23 +00:00
its-a-feature 653e5cb3ec adding option for inserting new lines when issuing tasking 2024-08-20 16:03:24 -05:00
github-actions 5756718433 Bump Mythic Dockerfile tag to match release 'v0.0.4.32' 2024-08-20 16:39:39 +00:00
its-a-feature 2df48e0d23 fixing an issue with task tags not showing text 2024-08-20 11:24:24 -05:00
github-actions 1a321aaad8 Bump Mythic Dockerfile tag to match release 'v0.0.4.31' 2024-08-20 02:28:04 +00:00
its-a-feature 1072fcfe9a more UI updates 2024-08-19 21:13:50 -05:00
github-actions 5ad5baa800 Bump Mythic Dockerfile tag to match release 'v0.0.4.30' 2024-08-19 15:57:20 +00:00
its-a-feature e6ada84bef fixing UI parsing issue 2024-08-19 10:43:21 -05:00
github-actions d59df9ab1f Bump Mythic Dockerfile tag to match release 'v0.0.4.29' 2024-08-17 14:03:47 +00:00
its-a-feature 25d3150f6f ini highlight support 2024-08-17 08:49:43 -05:00
github-actions d1f1730d77 Bump Mythic Dockerfile tag to match release 'v0.0.4.28' 2024-08-14 19:54:16 +00:00
its-a-feature eea163dcfe fixing console view for tasks 2024-08-14 14:40:11 -05:00
github-actions 1396906d96 Bump Mythic Dockerfile tag to match release 'v0.0.4.27' 2024-08-14 15:49:04 +00:00
its-a-feature 3443536d7c small UI tweaks 2024-08-14 10:34:44 -05:00
github-actions 11ded6c52c Bump Mythic Dockerfile tag to match release 'v0.0.4.26' 2024-08-14 14:10:15 +00:00
its-a-feature c79fb57a2d Few more UI fixes and tweaks around sorting tables 2024-08-14 08:55:52 -05:00
github-actions 5902a6c22a Bump Mythic Dockerfile tag to match release 'v0.0.4.25' 2024-08-13 23:02:54 +00:00
its-a-feature 25c183ac6e Callback Table, Process Browser, and File Browser updates 2024-08-13 17:48:34 -05:00
github-actions 5b04b61bb0 Bump Mythic Dockerfile tag to match release 'v0.0.4.24' 2024-08-12 18:44:13 +00:00
its-a-feature 96c61c7dfb File browser fixes 2024-08-12 13:30:09 -05:00
github-actions 69e714c96e Bump Mythic Dockerfile tag to match release 'v0.0.4.23' 2024-08-12 02:02:28 +00:00
its-a-feature cf40b2bfda More UI fixes for the file browser 2024-08-11 20:48:32 -05:00
github-actions 6dcca28fae Bump Mythic Dockerfile tag to match release 'v0.0.4.22' 2024-08-10 00:38:32 +00:00
its-a-feature c0b2709580 Merge branch 'Mythic3.3' of https://github.com/its-a-feature/Mythic into Mythic3.3 2024-08-09 19:24:08 -05:00
its-a-feature cdb7a0cd63 many updates for the file browser and callbacks table 2024-08-09 19:23:48 -05:00
github-actions 0c9b0c5fbe Bump Mythic Dockerfile tag to match release 'v0.0.4.21' 2024-08-08 20:59:17 +00:00
its-a-feature dca9444fc6 more UI tweaks and additional controls for active callbacks page 2024-08-08 15:45:21 -05:00
github-actions 271e4236a0 Bump Mythic Dockerfile tag to match release 'v0.0.4.20' 2024-08-07 20:33:12 +00:00
its-a-feature 03757a4c4e ui file preview tweaks and code alignment fixes 2024-08-07 15:19:16 -05:00
github-actions cc404823cc Bump Mythic Dockerfile tag to match release 'v0.0.4.19' 2024-08-06 22:47:25 +00:00
its-a-feature e0eab278dd update last checkin processing for linked callbacks 2024-08-06 17:33:13 -05:00
github-actions 46be2e2a9c Bump Mythic Dockerfile tag to match release 'v0.0.4.18' 2024-08-06 16:25:58 +00:00
its-a-feature 16aeafbfb6 fixing scrolling for console view and jupyter permissions 2024-08-06 11:11:24 -05:00
github-actions 1f368b4b37 Bump Mythic Dockerfile tag to match release 'v0.0.4.17' 2024-08-05 23:32:31 +00:00
its-a-feature ffb5a4f927 slight update to rpfwd timeouts 2024-08-05 18:17:58 -05:00
github-actions 8098cee611 Bump Mythic Dockerfile tag to match release 'v0.0.4.16' 2024-08-05 22:09:05 +00:00
its-a-feature 7ae5995de6 ui fixes, testProxy feature 2024-08-05 16:55:21 -05:00
github-actions c42d91edc6 Bump Mythic Dockerfile tag to match release 'v0.0.4.15' 2024-08-05 14:20:24 +00:00
its-a-feature 9aee9a3452 small ui tweaks 2024-08-05 09:06:28 -05:00
github-actions e32028964b Bump Mythic Dockerfile tag to match release 'v0.0.4.14' 2024-08-02 22:19:54 +00:00
its-a-feature 7583b78d70 updating streaming of filebrowser data 2024-08-02 17:05:18 -05:00
github-actions 28d87c5898 Bump Mythic Dockerfile tag to match release 'v0.0.4.13' 2024-08-02 18:07:35 +00:00
its-a-feature 7cc98709cc few UI tweaks and bug fixes 2024-08-02 12:53:25 -05:00
launchdaemon 7c3db07789 Update install_docker_ubuntu.sh
Check system architecture to add appropriate repository
2024-08-02 10:56:49 +01:00
launchdaemon 967bdbdb98 Update install_docker_debian.sh
Checks machine architecture to add the appropriate repository
2024-08-02 10:53:20 +01:00
github-actions a325ec1f88 Bump Mythic Dockerfile tag to match release 'v0.0.4.12' 2024-08-01 22:37:19 +00:00
its-a-feature 85f9c10617 updating mythic-cli defaults and echoing error messages from tasking 2024-08-01 17:23:24 -05:00
github-actions d80ca96dfa Bump Mythic Dockerfile tag to match release 'v0.0.4.11' 2024-07-31 22:38:52 +00:00
its-a-feature b28c918aad small fix for browser tables 2024-07-31 17:24:56 -05:00
github-actions 5cf2486f7f Bump Mythic Dockerfile tag to match release 'v0.0.4.10' 2024-07-31 21:33:04 +00:00
its-a-feature a563e0c78b more ui updates and mythic-cli additional options 2024-07-31 16:18:57 -05:00
github-actions cc7222f7ac Bump Mythic Dockerfile tag to match release 'v0.0.4.9' 2024-07-30 22:28:44 +00:00
its-a-feature 9c47d55de9 Many ui updates 2024-07-30 17:14:43 -05:00
github-actions 399b9800c3 Bump Mythic Dockerfile tag to match release 'v0.0.4.8' 2024-07-30 13:19:33 +00:00
its-a-feature 115ab27f56 quick fix for missing check with rendering browser scripts 2024-07-30 07:55:55 -05:00
github-actions 5c6aacb95c Bump Mythic Dockerfile tag to match release 'v0.0.4.7' 2024-07-30 00:12:18 +00:00
its-a-feature bdc4d2f79b many UI tweaks and updates 2024-07-29 18:58:03 -05:00
github-actions 32d9d76faa Bump Mythic Dockerfile tag to match release 'v0.0.4.6' 2024-07-29 17:32:42 +00:00
its-a-feature 07bb9f43f1 updating file browser buttons 2024-07-29 12:18:46 -05:00
github-actions 23d9ecb4dc Bump Mythic Dockerfile tag to match release 'v0.0.4.5' 2024-07-29 16:48:51 +00:00
its-a-feature a75420a272 fixing issue with postgres use_volume missing config 2024-07-29 11:34:26 -05:00
its-a-feature 9ecf431cd8 UI performance and feature updates 2024-07-29 11:24:16 -05:00
github-actions f557670077 Bump Mythic Dockerfile tag to match release 'v0.0.4.4' 2024-07-17 22:08:11 +00:00
its-a-feature c355318b03 fixing export payload config button 2024-07-17 16:52:53 -05:00
github-actions 29e0aeee5d Bump Mythic Dockerfile tag to match release 'v0.0.4.3' 2024-07-17 14:08:53 +00:00
its-a-feature a2135e93ba fixing small bug in UI when viewing C2 Profile build parameters 2024-07-17 08:54:52 -05:00
github-actions 2203c84e12 Bump Mythic Dockerfile tag to match release 'v0.0.4.2' 2024-07-14 14:51:11 +00:00
its-a-feature abf69f6e97 bug fix with mythic scripting
updating to v3.3.0-rc3
2024-07-14 09:37:21 -05:00
github-actions eea88d8860 Bump Mythic Dockerfile tag to match release 'v0.0.4.1' 2024-07-11 14:44:09 +00:00
its-a-feature b77077b06d Bug fixes for file searching and task completion 2024-07-11 09:30:00 -05:00
github-actions 5df6d0470f Bump Mythic Dockerfile tag to match release 'v0.0.4.0' 2024-07-09 23:29:12 +00:00
its-a-feature 8dbbd028da updating max container version 2024-07-09 17:51:30 -05:00
its-a-feature e6c4792151 Mythic3.3.0 Initial push 2024-07-09 17:34:09 -05:00
dependabot[bot] 4c142b8791 Bump ejs from 3.1.9 to 3.1.10 in /MythicReactUI
Bumps [ejs](https://github.com/mde/ejs) from 3.1.9 to 3.1.10.
- [Release notes](https://github.com/mde/ejs/releases)
- [Commits](https://github.com/mde/ejs/compare/v3.1.9...v3.1.10)

---
updated-dependencies:
- dependency-name: ejs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2024-05-02 13:58:50 +00:00
github-actions 2079bd7ff0 Bump Mythic Dockerfile tag to match release 'v0.0.3.53' 2024-04-23 14:28:50 +00:00
its-a-feature 20199e5dc3 v3.2.20-rc11
fixing small bug with syncing commands over
2024-04-23 08:59:36 -05:00
github-actions 6a43c2439b Bump Mythic Dockerfile tag to match release 'v0.0.3.52' 2024-04-10 16:08:10 +00:00
its-a-feature 918f9e2e93 v3.2.20-rc10
fixing a few errors in MythicRPC calls when fetching data from database
2024-04-10 10:53:58 -05:00
github-actions 9d8a361c48 Bump Mythic Dockerfile tag to match release 'v0.0.3.51' 2024-04-09 13:09:16 +00:00
its-a-feature 5a34ffc7c0 fixing mythic-cli issue 2024-04-09 07:55:41 -05:00
github-actions 1d2f6a5a61 Bump Mythic Dockerfile tag to match release 'v0.0.3.50' 2024-04-09 01:14:18 +00:00
its-a-feature 4ed5172b30 v3.2.20-rc9
Fixed SendMythicRPCTaskSearch and added new data sent out for logging
2024-04-08 19:59:51 -05:00
github-actions f7a002edb7 Bump Mythic Dockerfile tag to match release 'v0.0.3.49' 2024-04-05 23:09:07 +00:00
its-a-feature 8bfc904dc6 v3.2.20-rc8
fixing MythicRPC issue for generating payloads from scratch
2024-04-05 17:55:17 -05:00
github-actions f923eb17a2 Bump Mythic Dockerfile tag to match release 'v0.0.3.48' 2024-03-30 23:37:38 +00:00
its-a-feature 7535e55317 More UI array fixes
also auto select c2 profile when selecting saved c2 instance
2024-03-30 18:23:54 -05:00
github-actions 282cd32d78 Bump Mythic Dockerfile tag to match release 'v0.0.3.47' 2024-03-30 20:59:00 +00:00
its-a-feature 08c808ea33 fixing issue with parsing saved arrays from c2 instances 2024-03-30 15:45:15 -05:00
github-actions 7c0dfacba9 Bump Mythic Dockerfile tag to match release 'v0.0.3.46' 2024-03-30 15:29:26 +00:00
its-a-feature 4cf6d9d8fa fixing an issue with callbacks not getting hidden from view 2024-03-30 10:15:34 -05:00
github-actions d2786bab86 Bump Mythic Dockerfile tag to match release 'v0.0.3.45' 2024-03-30 14:40:01 +00:00
its-a-feature 7349b29e71 forgot to update the version file 2024-03-30 09:26:14 -05:00
github-actions bb0d9044ab Bump Mythic Dockerfile tag to match release 'v0.0.3.44' 2024-03-29 21:53:55 +00:00
its-a-feature 1cf8948320 v3.2.20-rc7
added more c2 profile inner server restarts in case of file changes
2024-03-29 16:30:45 -05:00
its-a-feature d7c42e739f updating broken slack invite link 2024-03-27 12:07:03 -05:00
github-actions 2cee0f7cba Bump Mythic Dockerfile tag to match release 'v0.0.3.43' 2024-03-25 19:44:15 +00:00
its-a-feature 08cdbdb312 updating UI Search features and results 2024-03-25 14:29:30 -05:00
github-actions 6936e5a3d6 Bump Mythic Dockerfile tag to match release 'v0.0.3.42' 2024-03-25 13:36:47 +00:00
its-a-feature af0dcbec44 v0.3.20-rc6
Adding PushC2OneToMany functionality
2024-03-25 08:07:42 -05:00
github-actions dfd3715409 Bump Mythic Dockerfile tag to match release 'v0.0.3.41' 2024-03-21 20:29:44 +00:00
its-a-feature df457ae7fb fixing incompatible changes in mythic-cli go docker library 2024-03-21 14:59:54 -05:00
its-a-feature bfac6643d4 initial pass for one-to-many pushc2 WIP 2024-03-21 14:41:58 -05:00
its-a-feature 19ebe8a78c fixing bug in UI experimental table filters 2024-03-21 14:35:21 -05:00
github-actions b830bd247a Bump Mythic Dockerfile tag to match release 'v0.0.3.40' 2024-03-19 23:32:09 +00:00
its-a-feature 0343bcabb0 v3.2.20-rc5
UI Bug fix with Streaming Now and alpha support for XML messages natively
2024-03-19 18:17:41 -05:00
its-a-feature b589855d06 v3.2.20-rc4
more socks debugging
2024-03-19 13:09:57 -05:00
github-actions a24d7f6be5 Bump Mythic Dockerfile tag to match release 'v0.0.3.39' 2024-03-19 16:03:48 +00:00
its-a-feature 644f6e96be v3.2.20-rc3
SOCKS and logging tweaks
2024-03-19 10:49:19 -05:00
github-actions f3564a8ad4 Bump Mythic Dockerfile tag to match release 'v0.0.3.38' 2024-03-14 19:24:59 +00:00
its-a-feature 448285f49b v3.2.20-rc2
fixed rpfwd tracking
2024-03-14 14:05:59 -05:00
github-actions d108790325 Bump Mythic Dockerfile tag to match release 'v0.0.3.37' 2024-03-08 15:33:38 +00:00
its-a-feature 23e5c2f8b1 v3.2.20-rc1
fixed issue with stat tracking for new socks/rpfwd ports
2024-03-08 09:16:32 -06:00
github-actions 5b67741a0f Bump Mythic Dockerfile tag to match release 'v0.0.3.36' 2024-03-06 20:47:30 +00:00
its-a-feature e204be77e5 fixing UI task button bug 2024-03-06 14:33:01 -06:00
github-actions a8b8bfb76c Bump Mythic Dockerfile tag to match release 'v0.0.3.35' 2024-03-06 14:16:41 +00:00
its-a-feature 56846d2278 v3.2.19
Adding more fields to tasking callback data and adding try/catch for slider parsing on the dashboard
2024-03-06 08:02:31 -06:00
github-actions 2068749e81 Bump Mythic Dockerfile tag to match release 'v0.0.3.34' 2024-03-05 00:18:01 +00:00
its-a-feature 2970729709 v3.2.18-rc10
UI bug fixes and agent_type support added in
2024-03-04 18:03:37 -06:00
github-actions 287e95990b Bump Mythic Dockerfile tag to match release 'v0.0.3.33' 2024-02-29 21:57:08 +00:00
its-a-feature 43f11e5a4e v0.0.3.33
UI Fixes for files search upload/download/screenshot page
2024-02-29 15:42:55 -06:00
github-actions 84a68d94ad Bump Mythic Dockerfile tag to match release 'v0.0.3.32' 2024-02-29 21:28:37 +00:00
its-a-feature bfcf5c2a7c v0.0.3.32
UI Updates
2024-02-29 15:14:08 -06:00
github-actions 105fd5c36a Bump Mythic Dockerfile tag to match release 'v0.0.3.31' 2024-02-28 23:36:33 +00:00
its-a-feature b43b2b3fb7 Merge branch 'master' of https://github.com/its-a-feature/Mythic 2024-02-28 17:21:40 -06:00
its-a-feature 59e9a9efc4 v3.2.18-rc9
UI Fixes/updates and updating server to respond successfully even if post response data fails to process
2024-02-28 17:21:37 -06:00
github-actions ad82245016 Bump Mythic Dockerfile tag to match release 'v0.0.3.30' 2024-02-26 13:42:30 +00:00
its-a-feature f941848f60 v3.2.18-rc8
various UI fixes
2024-02-26 06:25:27 -07:00
github-actions 85b1cdf5ae Bump Mythic Dockerfile tag to match release 'v0.0.3.29' 2024-02-21 22:36:53 +00:00
its-a-feature 29b3c89948 v3.2.18-rc7
Added `import` and `export` features to c2 profile instances
2024-02-21 16:22:12 -06:00
github-actions 21f799de67 Bump Mythic Dockerfile tag to match release 'v0.0.3.28' 2024-02-20 21:04:31 +00:00
its-a-feature c3cf3f986f updating mythic-cli
fixing bug with documentation container for wrapper payloads
2024-02-20 14:50:26 -06:00
github-actions d11cdecf48 Bump Mythic Dockerfile tag to match release 'v0.0.3.27' 2024-02-19 20:10:01 +00:00
its-a-feature b6f60329a1 v3.2.18-rc6
updated a check for parent-path parsing in file browser data
2024-02-19 13:56:14 -06:00
github-actions d130595be7 Bump Mythic Dockerfile tag to match release 'v0.0.3.26' 2024-02-19 18:39:31 +00:00
its-a-feature 28f6acbc93 Fixing experimental callbacks table rendering 2024-02-19 12:24:15 -06:00
github-actions 5dda200b1f Bump Mythic Dockerfile tag to match release 'v0.0.3.25' 2024-02-16 23:15:09 +00:00
its-a-feature 0a69f83969 UI fixes and slight server update 2024-02-16 16:58:08 -06:00
github-actions 9af5b64770 Bump Mythic Dockerfile tag to match release 'v0.0.3.24' 2024-02-15 14:29:46 +00:00
its-a-feature b5cfc05dae Merge branch 'master' of https://github.com/its-a-feature/Mythic 2024-02-15 08:15:24 -06:00
its-a-feature fd71b1c752 fixing UI bug with media output types 2024-02-15 08:15:20 -06:00
github-actions 02e3f5f53a Bump Mythic Dockerfile tag to match release 'v0.0.3.23' 2024-02-15 00:15:42 +00:00
its-a-feature c9c2aaccc2 fixing UI multi-tasking bug
updated callbacks table with new table when experimental setting flipped
2024-02-14 18:01:40 -06:00
github-actions 76c929d244 Bump Mythic Dockerfile tag to match release 'v0.0.3.22' 2024-02-14 17:30:23 +00:00
its-a-feature 0a51910ce0 UI bug fixes 2024-02-14 11:15:50 -06:00
github-actions 4c5efe84b2 Bump Mythic Dockerfile tag to match release 'v0.0.3.21' 2024-02-14 00:28:10 +00:00
its-a-feature 5b112635c9 updating mythic-cli to remove react ui volume as necessary
also fixing bad golang type when installing services
2024-02-13 18:13:14 -06:00
github-actions 643aaf472b Bump Mythic Dockerfile tag to match release 'v0.0.3.20' 2024-02-13 23:25:37 +00:00
its-a-feature d40e3a57b5 fixing bug in UI for choosing commands 2024-02-13 17:11:52 -06:00
github-actions e7e00b4b7f Bump Mythic Dockerfile tag to match release 'v0.0.3.19' 2024-02-13 22:48:45 +00:00
its-a-feature a706381ca0 updating mythic-cli labels and logging 2024-02-13 16:34:42 -06:00
github-actions 7ee5676630 Bump Mythic Dockerfile tag to match release 'v0.0.3.18' 2024-02-13 21:53:53 +00:00
its-a-feature ce2843b801 v3.2.18-rc4
adding user secrets, updating callback imports, tweaking socks
2024-02-13 15:32:36 -06:00
github-actions d6c32d0681 Bump Mythic Dockerfile tag to match release 'v0.0.3.17' 2024-02-11 02:34:08 +00:00
its-a-feature dcebe2686a updating psotgres connection data
updating mythic-cli to add command for postgres connection
2024-02-10 20:18:33 -06:00
github-actions 0bd89aca4b Bump Mythic Dockerfile tag to match release 'v0.0.3.16' 2024-02-11 00:01:23 +00:00
its-a-feature d7924f966d mythic-cli updates for better handling of postgres ports
updating postgres' dockerfile to properly expose changed ports
2024-02-10 17:47:12 -06:00
github-actions 774ea60b78 Bump Mythic Dockerfile tag to match release 'v0.0.3.15' 2024-02-10 03:15:26 +00:00
its-a-feature ed464263cb fixing postgres port bug 2024-02-09 21:01:24 -06:00
github-actions 63c3af046d Bump Mythic Dockerfile tag to match release 'v0.0.3.14' 2024-02-09 23:18:37 +00:00
its-a-feature cc9169d91a v3.2.18-rc3 2024-02-09 16:56:19 -06:00
github-actions c9b9f94b75 Bump Mythic Dockerfile tag to match release 'v0.0.3.13' 2024-02-08 18:37:02 +00:00
its-a-feature d71fe73fee v3.2.18-rc2
fixed issue with sometimes task status not getting updated when also setting complete
2024-02-08 12:22:36 -06:00
its-a-feature 4c1c28da66 updating version to make mythic-cli happy 2024-02-08 11:33:43 -06:00
github-actions 81b298793d Bump Mythic Dockerfile tag to match release 'v0.0.3.12' 2024-02-08 16:35:29 +00:00
its-a-feature 3ee407b1d9 v3.2.17.1
added some better checking and deadlock preventing checks around socks traffic
2024-02-08 10:20:08 -06:00
github-actions c23998affc Bump Mythic Dockerfile tag to match release 'v0.0.3.11' 2024-02-07 20:19:21 +00:00
its-a-feature f5890049fa updating mythic-cli for better local-building
updating UI plaintext view to be more streamlined
2024-02-07 13:53:14 -06:00
github-actions c3d443ea91 Bump Mythic Dockerfile tag to match release 'v0.0.3.10' 2024-02-07 14:17:50 +00:00
its-a-feature 25afd25cc0 fixing mythic-cli typo breaking fresh installs 2024-02-07 08:03:07 -06:00
github-actions 59de20dac7 Bump Mythic Dockerfile tag to match release 'v0.0.3.9' 2024-02-06 21:32:59 +00:00
its-a-feature af58bc9dbc v3.2.17
bug fixes and new features for mythic-cli, mythic_server, and the UI
2024-02-06 15:04:04 -06:00
github-actions 2aa0c39888 Bump Mythic Dockerfile tag to match release 'v0.0.3.8' 2024-01-31 00:25:12 +00:00
its-a-feature 93cec09443 updating mythic-cli copying without -it for non TTYs 2024-01-30 16:12:09 -08:00
github-actions 29758798d1 Bump Mythic Dockerfile tag to match release 'v0.0.3.7' 2024-01-30 23:56:40 +00:00
its-a-feature eadb076f0b updating mythic-cli for handling non-tty install 2024-01-30 15:43:39 -08:00
github-actions 7e27b82f9f Bump Mythic Dockerfile tag to match release 'v0.0.3.6' 2024-01-29 22:55:38 +00:00
its-a-feature 43ccf85cbe fixing a bad link on the dashboard page for users/hosts 2024-01-29 14:42:54 -08:00
github-actions 1c903809ea Bump Mythic Dockerfile tag to match release 'v0.0.3.5' 2024-01-29 21:59:04 +00:00
its-a-feature ebef87de60 update mythic-cli to remove old volume when adding 3rd party services 2024-01-29 13:46:44 -08:00
github-actions 34ab89cc99 Bump Mythic Dockerfile tag to match release 'v0.0.3.4' 2024-01-29 20:08:44 +00:00
its-a-feature bab908ccf1 updated mythic-cli to not report no matching keys and success 2024-01-29 11:09:53 -08:00
its-a-feature c855cb2e0e updated mythic-cli's regex matching for getting/setting env variables 2024-01-29 11:06:34 -08:00
github-actions 3a8ed471e9 Bump Mythic Dockerfile tag to match release 'v0.0.3.3' 2024-01-29 16:14:52 +00:00
its-a-feature e73ab8ee56 UI and mythic-cli fixes 2024-01-29 07:51:12 -08:00
github-actions e90a5559be Bump Mythic Dockerfile tag to match release 'v0.0.3.2' 2024-01-29 01:18:34 +00:00
its-a-feature 5240ad3f6a updating to push updates to master branch 2024-01-28 17:06:05 -08:00
its-a-feature 96563b2978 updated mythic-cli
better mythic-cli support for mythic_graphql volumes
2024-01-28 16:46:12 -08:00
Cody Thomas 30856d7d4e Merge pull request #361 from its-a-feature/docker_updates
Docker updates
2024-01-28 16:37:00 -06:00
1117 changed files with 165046 additions and 623668 deletions

No files matched your search

+2
View File
@@ -2,9 +2,11 @@ version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
target-branch: "Mythic-v4.0.0"
schedule:
interval: "weekly"
- package-ecosystem: "gomod"
directory: "/"
target-branch: "Mythic-v4.0.0"
schedule:
interval: "weekly"
+60 -58
View File
@@ -9,7 +9,7 @@ on:
# types: [published]
push:
branches:
- 'docker_updates'
- 'Mythic-v4.0.0'
- 'master'
tags:
- "v*.*.*"
@@ -43,8 +43,8 @@ env:
# Set the container image version to the Github release tag
VERSION: ${{ github.ref_name }}
# Branch for pushing release changes (TODO: Change this to the main branch when the rewrite is finished)
RELEASE_BRANCH: docker_updates
# Branch for pushing release changes
RELEASE_BRANCH: "Mythic-v4.0.0"
jobs:
# Builds the base container image and pushes it to the container registry
@@ -55,25 +55,27 @@ jobs:
packages: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4 # ref: https://github.com/marketplace/actions/checkout
uses: actions/checkout@v5 # ref: https://github.com/marketplace/actions/checkout
- name: Log in to the container registry
uses: docker/login-action@v3 # ref: https://github.com/marketplace/actions/docker-login
uses: docker/login-action@v4 # ref: https://github.com/marketplace/actions/docker-login
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
with:
platforms: 'arm64,arm'
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
# the following are unique to this job
- name: Lowercase the server container image name
run: echo "MYTHIC_SERVER_IMAGE_NAME=${MYTHIC_SERVER_IMAGE_NAME,,}" >> ${GITHUB_ENV}
- name: Copy version file over
run: cp VERSION mythic-docker/src/VERSION
- name: Build and push the server container image
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
uses: docker/build-push-action@v7 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
with:
context: mythic-docker
file: mythic-docker/.docker/Dockerfile
@@ -97,28 +99,28 @@ jobs:
steps:
# Pull in the repository code
- name: Checkout the repository
uses: actions/checkout@v4 # ref: https://github.com/marketplace/actions/checkout
uses: actions/checkout@v5 # ref: https://github.com/marketplace/actions/checkout
- name: Log in to the container registry
uses: docker/login-action@v3 # ref: https://github.com/marketplace/actions/docker-login
uses: docker/login-action@v4 # ref: https://github.com/marketplace/actions/docker-login
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
with:
platforms: 'arm64,arm'
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
# the following are unique to this job
- name: Lowercase the postgres container image name
run: echo "MYTHIC_POSTGRES_IMAGE_NAME=${MYTHIC_POSTGRES_IMAGE_NAME,,}" >> ${GITHUB_ENV}
- name: Build and push the postgres container image
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
uses: docker/build-push-action@v7 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
with:
context: postgres-docker
file: postgres-docker/.docker/Dockerfile
file: postgres-docker/Dockerfile
tags: |
${{ env.REGISTRY }}/${{ env.MYTHIC_POSTGRES_IMAGE_NAME }}:${{ env.VERSION }}
${{ env.REGISTRY }}/${{ env.MYTHIC_POSTGRES_IMAGE_NAME }}:latest
@@ -139,28 +141,28 @@ jobs:
steps:
# Pull in the repository code
- name: Checkout the repository
uses: actions/checkout@v4 # ref: https://github.com/marketplace/actions/checkout
uses: actions/checkout@v5 # ref: https://github.com/marketplace/actions/checkout
- name: Log in to the container registry
uses: docker/login-action@v3 # ref: https://github.com/marketplace/actions/docker-login
uses: docker/login-action@v4 # ref: https://github.com/marketplace/actions/docker-login
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
with:
platforms: 'arm64,arm'
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
# the following are unique to this job
- name: Lowercase the rabbitmq container image name
run: echo "MYTHIC_RABBITMQ_IMAGE_NAME=${MYTHIC_RABBITMQ_IMAGE_NAME,,}" >> ${GITHUB_ENV}
- name: Build and push the rabbitmq container image
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
uses: docker/build-push-action@v7 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
with:
context: rabbitmq-docker
file: rabbitmq-docker/.docker/Dockerfile
file: rabbitmq-docker/Dockerfile
tags: |
${{ env.REGISTRY }}/${{ env.MYTHIC_RABBITMQ_IMAGE_NAME }}:${{ env.VERSION }}
${{ env.REGISTRY }}/${{ env.MYTHIC_RABBITMQ_IMAGE_NAME }}:latest
@@ -181,28 +183,28 @@ jobs:
steps:
# Pull in the repository code
- name: Checkout the repository
uses: actions/checkout@v4 # ref: https://github.com/marketplace/actions/checkout
uses: actions/checkout@v5 # ref: https://github.com/marketplace/actions/checkout
- name: Log in to the container registry
uses: docker/login-action@v3 # ref: https://github.com/marketplace/actions/docker-login
uses: docker/login-action@v4 # ref: https://github.com/marketplace/actions/docker-login
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
with:
platforms: 'arm64,arm'
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
# the following are unique to this job
- name: Lowercase the documentation container image name
run: echo "MYTHIC_DOCUMENTATION_IMAGE_NAME=${MYTHIC_DOCUMENTATION_IMAGE_NAME,,}" >> ${GITHUB_ENV}
- name: Build and push the documentation container image
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
uses: docker/build-push-action@v7 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
with:
context: documentation-docker
file: documentation-docker/.docker/Dockerfile
file: documentation-docker/Dockerfile
tags: |
${{ env.REGISTRY }}/${{ env.MYTHIC_DOCUMENTATION_IMAGE_NAME }}:${{ env.VERSION }}
${{ env.REGISTRY }}/${{ env.MYTHIC_DOCUMENTATION_IMAGE_NAME }}:latest
@@ -223,30 +225,30 @@ jobs:
steps:
# Pull in the repository code
- name: Checkout the repository
uses: actions/checkout@v4 # ref: https://github.com/marketplace/actions/checkout
uses: actions/checkout@v5 # ref: https://github.com/marketplace/actions/checkout
- name: Log in to the container registry
uses: docker/login-action@v3 # ref: https://github.com/marketplace/actions/docker-login
uses: docker/login-action@v4 # ref: https://github.com/marketplace/actions/docker-login
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
with:
platforms: 'arm64,arm'
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
# these two are unique
- name: Lowercase the jupyter container image name
run: echo "MYTHIC_JUPYTER_IMAGE_NAME=${MYTHIC_JUPYTER_IMAGE_NAME,,}" >> ${GITHUB_ENV}
- name: Build and push the jupyter container image
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
uses: docker/build-push-action@v7 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
with:
context: jupyter-docker
file: jupyter-docker/.docker/Dockerfile
file: jupyter-docker/Dockerfile
tags: |
${{ env.REGISTRY }}/${{ env.MYTHIC_JUPYTER_IMAGE_NAME }}:${{ env.VERSION }}
${{ env.REGISTRY }}/${{ env.MYTHIC_JUPYTER_IMAGE_NAME }}:latest
@@ -267,30 +269,30 @@ jobs:
steps:
# Pull in the repository code
- name: Checkout the repository
uses: actions/checkout@v4 # ref: https://github.com/marketplace/actions/checkout
uses: actions/checkout@v5 # ref: https://github.com/marketplace/actions/checkout
- name: Log in to the container registry
uses: docker/login-action@v3 # ref: https://github.com/marketplace/actions/docker-login
uses: docker/login-action@v4 # ref: https://github.com/marketplace/actions/docker-login
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
with:
platforms: 'arm64,arm'
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
# these two are unique
- name: Lowercase the graphql container image name
run: echo "MYTHIC_GRAPHQL_IMAGE_NAME=${MYTHIC_GRAPHQL_IMAGE_NAME,,}" >> ${GITHUB_ENV}
- name: Build and push the graphql container image
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
uses: docker/build-push-action@v7 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
with:
context: hasura-docker
file: hasura-docker/.docker/Dockerfile
file: hasura-docker/Dockerfile
tags: |
${{ env.REGISTRY }}/${{ env.MYTHIC_GRAPHQL_IMAGE_NAME }}:${{ env.VERSION }}
${{ env.REGISTRY }}/${{ env.MYTHIC_GRAPHQL_IMAGE_NAME }}:latest
@@ -311,30 +313,30 @@ jobs:
steps:
# Pull in the repository code
- name: Checkout the repository
uses: actions/checkout@v4 # ref: https://github.com/marketplace/actions/checkout
uses: actions/checkout@v5 # ref: https://github.com/marketplace/actions/checkout
- name: Log in to the container registry
uses: docker/login-action@v3 # ref: https://github.com/marketplace/actions/docker-login
uses: docker/login-action@v4 # ref: https://github.com/marketplace/actions/docker-login
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
with:
platforms: 'arm64,arm'
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
# these two are unique
- name: Lowercase the nginx container image name
run: echo "MYTHIC_NGINX_IMAGE_NAME=${MYTHIC_NGINX_IMAGE_NAME,,}" >> ${GITHUB_ENV}
- name: Build and push the nginx container image
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
uses: docker/build-push-action@v7 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
with:
context: nginx-docker
file: nginx-docker/.docker/Dockerfile
file: nginx-docker/Dockerfile
tags: |
${{ env.REGISTRY }}/${{ env.MYTHIC_NGINX_IMAGE_NAME }}:${{ env.VERSION }}
${{ env.REGISTRY }}/${{ env.MYTHIC_NGINX_IMAGE_NAME }}:latest
@@ -355,29 +357,29 @@ jobs:
steps:
# Pull in the repository code
- name: Checkout the repository
uses: actions/checkout@v4 # ref: https://github.com/marketplace/actions/checkout
uses: actions/checkout@v5 # ref: https://github.com/marketplace/actions/checkout
- name: Log in to the container registry
uses: docker/login-action@v3 # ref: https://github.com/marketplace/actions/docker-login
uses: docker/login-action@v4 # ref: https://github.com/marketplace/actions/docker-login
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
with:
platforms: 'arm64,arm'
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
# these two are unique
- name: Lowercase the react container image name
run: echo "MYTHIC_REACT_IMAGE_NAME=${MYTHIC_REACT_IMAGE_NAME,,}" >> ${GITHUB_ENV}
- name: Build and push the react container image
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
uses: docker/build-push-action@v7 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
with:
context: mythic-react-docker
context: .
file: mythic-react-docker/.docker/Dockerfile
tags: |
${{ env.REGISTRY }}/${{ env.MYTHIC_REACT_IMAGE_NAME }}:${{ env.VERSION }}
@@ -409,20 +411,20 @@ jobs:
steps:
# Pull in the repository code
- name: Checkout the repository
uses: actions/checkout@v4 # ref: https://github.com/marketplace/actions/checkout
uses: actions/checkout@v5 # ref: https://github.com/marketplace/actions/checkout
- name: Log in to the container registry
uses: docker/login-action@v3 # ref: https://github.com/marketplace/actions/docker-login
uses: docker/login-action@v4 # ref: https://github.com/marketplace/actions/docker-login
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
with:
platforms: 'arm64,arm'
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
# the following are unique to this job
- name: Lowercase the cli container image name
run: echo "MYTHIC_CLI_IMAGE_NAME=${MYTHIC_CLI_IMAGE_NAME,,}" >> ${GITHUB_ENV}
@@ -433,10 +435,10 @@ jobs:
sed -i "s|^const MythicDockerLatest =.*$|const MythicDockerLatest = \"${VERSION}\"|" constants.go
- name: Build and push the cli container image
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
uses: docker/build-push-action@v7 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
with:
context: Mythic_CLI
file: Mythic_CLI/.docker/Dockerfile
file: Mythic_CLI/Dockerfile
tags: |
${{ env.REGISTRY }}/${{ env.MYTHIC_CLI_IMAGE_NAME }}:${{ env.VERSION }}
${{ env.REGISTRY }}/${{ env.MYTHIC_CLI_IMAGE_NAME }}:latest
@@ -468,7 +470,7 @@ jobs:
steps:
# Pull in the repository code
- name: Checkout the repository
uses: actions/checkout@v4 # ref: https://github.com/marketplace/actions/checkout
uses: actions/checkout@v5 # ref: https://github.com/marketplace/actions/checkout
# update names to lowercase
- name: Lowercase the server container image name
@@ -548,7 +550,7 @@ jobs:
uses: EndBug/add-and-commit@v9 # ref: https://github.com/marketplace/actions/add-commit
with:
# Only add the Dockerfile changes. Nothing else should have been modified
add: "['mythic-docker/Dockerfile', 'Mythic_CLI/Dockerfile', 'Mythic_CLI/Makefile', 'Mythic_CLI/src/cmd/config/constants.go', 'postgres-docker/Dockerfile', 'rabbitmq-docker/Dockerfile', 'documentation-docker/Dockerfile', 'jupyter-docker/Dockerfile', 'hasura-docker/Dockerfile', 'nginx-docker/Dockerfile', 'mythic-react-docker/Dockerfile']"
add: "['Mythic_CLI/Makefile', 'Mythic_CLI/src/cmd/config/constants.go']"
# Use the Github actions bot for the commit author
default_author: github_actions
committer_email: github-actions[bot]@users.noreply.github.com
+12
View File
@@ -25,6 +25,7 @@ venv.bak/
# ssl certs
ssl/
# Mythic files
nginx-docker/config/blockips.conf
mythic-docker/src/Mythic
mythic_access.*
mythic_sync/
@@ -37,6 +38,7 @@ Docker_Templates/
documentation-docker/content/
documentation-docker/public/
display_output.txt
full_attack.json
nginx-docker/config/conf.d/services.conf
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
@@ -163,3 +165,13 @@ _ReSharper*/
.vscode/
jupyter-docker/jupyter/MythicExamples/.ipynb_checkpoints/
nginx-docker/config/blockips.conf
/MythicReactUI/build/
/MythicReactUI/node_modules/
/mythic-react-docker/mythic/public/
/.gocache/
.gocache/
MythicReactUI/build
MythicReactUI/node_modules
+1507 -1
View File
File diff suppressed because it is too large. Load diff
+46 -1
View File
@@ -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.
+9 -3
View File
@@ -2,10 +2,16 @@
default: linux ;
linux:
cd Mythic_CLI && make && mv mythic-cli ../
cd Mythic_CLI && make build_linux && mv mythic-cli ../
macos:
cd Mythic_CLI && make build_macos && mv mythic-cli ../
macos_local:
cd Mythic_CLI && make build_binary_macos_local
local:
cd Mythic_CLI && make build_local
linux_docker:
cd Mythic_CLI && make build_linux_docker && mv mythic-cli ../
macos_docker:
cd Mythic_CLI && make build_macos_docker && mv mythic-cli ../
+20
View File
@@ -0,0 +1,20 @@
*
!Dockerfile
!package.json
!package-lock.json
!config-overrides.js
!public/
!public/**
!src/
!src/**
!.env
.DS_Store
**/.DS_Store
node_modules/
build/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.npm/
.cache/
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:20-alpine
FROM node:24-alpine
WORKDIR /app
+26
View File
@@ -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;
}
+8070 -5994
View File
File diff suppressed because it is too large. Load diff
+50 -42
View File
@@ -6,66 +6,74 @@
"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.8.6",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-brands-svg-icons": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@mui/icons-material": "^5.14.16",
"@mui/lab": "^5.0.0-alpha.138",
"@mui/material": "^5.14.16",
"@mui/x-charts": "^6.18.7",
"@mui/x-data-grid": "^6.18.6",
"@mui/x-date-pickers": "^6.17.0",
"ace-builds": "^1.31.1",
"anser": "^2.1.1",
"dayjs": "^1.11.10",
"elkjs": "^0.8.2",
"graphql": "^16.8.1",
"graphql-ws": "^5.14.2",
"@apollo/client": "^3.13.8",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@hello-pangea/dnd": "^17.0.0",
"@mui/icons-material": "^7.0.2",
"@mui/lab": "^7.0.0-beta.11",
"@mui/material": "^7.0.2",
"@mui/utils": "^7.0.2",
"@mui/x-charts": "^8.1.0",
"@mui/x-date-pickers": "^8.1.0",
"@xterm/addon-fit": "^0.11.0",
"@xterm/xterm": "^6.0.0",
"@xyflow/react": "^12.6.0",
"ace-builds": "^1.40.1",
"anser": "^2.3.2",
"dayjs": "^1.11.13",
"dompurify": "^3.4.17",
"elkjs": "^0.10.0",
"graphql": "^16.10.0",
"graphql-ws": "^6.0.4",
"html-to-image": "^1.11.11",
"html2canvas": "^1.4.1",
"ip6": "^0.2.10",
"json5": "^2.2.3",
"jwt-decode": "^3.1.2",
"material-react-table": "^2.9.2",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
"jwt-decode": "^4.0.0",
"path-browserify": "^1.0.1",
"randexp": "^0.5.3",
"react": "^18.2.0",
"react-ace": "^10.1.0",
"react": "^18.3.1",
"react-ace": "^14.0.1",
"react-colorful": "^5.6.1",
"react-dom": "^18.2.0",
"react-dom": "^18.3.1",
"react-draggable": "^4.4.6",
"react-moment": "^1.1.3",
"react-router-dom": "^6.18.0",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.13.1",
"react-scrollbar-size": "^5.0.0",
"react-split": "^2.0.14",
"react-toastify": "^9.1.3",
"react-virtualized": "^9.22.5",
"react-virtualized-auto-sizer": "^1.0.20",
"react-window": "^1.8.9",
"reactflow": "^11.9.4",
"react-toastify": "^11.0.5",
"react-tooltip": "^5.28.1",
"react-virtualized": "^9.22.6",
"react-virtualized-auto-sizer": "^1.0.26",
"react-window": "^1.8.11",
"remark-gfm": "^4.0.1",
"save-svg-as-png": "^1.4.17",
"semver": "^7.5.4",
"styled-components": "^6.1.0",
"tough-cookie": "^4.1.3",
"semver": "^7.6.2",
"sql.js": "^1.13.0",
"styled-components": "^6.1.17",
"uuid": "^9.0.1",
"webpack": "^5.89.0",
"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": [
@@ -0,0 +1,61 @@
let browserScript;
let browserScriptSource;
let activeRequestID;
const serializeError = (error) => ({
name: (error && error.name) || "Error",
message: (error && error.message) || String(error),
stack: error && error.stack,
});
const postLog = (...args) => {
try {
self.postMessage({type: "log", requestID: activeRequestID, args});
} catch (error) {
self.postMessage({
type: "log",
requestID: activeRequestID,
args: args.map((arg) => String(arg)),
});
}
};
console.log = postLog;
self.onmessage = ({data}) => {
if(!data || data.type !== "render"){
return;
}
const {requestID, script, task, responses} = data;
activeRequestID = requestID;
let phase = "compile";
try {
if(typeof script === "string" && script !== browserScriptSource){
browserScript = undefined;
browserScriptSource = undefined;
browserScript = Function(`"use strict";return(${script})`)();
if(typeof browserScript !== "function"){
throw new TypeError("Browser script must evaluate to a function");
}
browserScriptSource = script;
}
if(typeof browserScript !== "function"){
throw new Error("No browser script is loaded");
}
phase = "execute";
const result = browserScript(task, responses);
if(result && typeof result.then === "function"){
throw new TypeError("Browser scripts must return their display data synchronously");
}
phase = "serialize";
self.postMessage({type: "result", requestID, result});
} catch (error) {
self.postMessage({type: "error", requestID, phase, error: serializeError(error)});
} finally {
activeRequestID = undefined;
}
};
+25
View File
@@ -21,6 +21,31 @@
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Mythic</title>
<script>
(function () {
var resizeObserverLoopErrors = [
"ResizeObserver loop limit exceeded",
"ResizeObserver loop completed with undelivered notifications."
];
function isResizeObserverLoopError(message) {
return resizeObserverLoopErrors.indexOf(message) !== -1;
}
window.addEventListener("error", function (event) {
if (isResizeObserverLoopError(event.message)) {
event.stopImmediatePropagation();
event.preventDefault();
}
}, true);
window.addEventListener("unhandledrejection", function (event) {
var reason = event.reason;
var message = reason && reason.message ? reason.message : reason;
if (isResizeObserverLoopError(message)) {
event.stopImmediatePropagation();
event.preventDefault();
}
}, true);
})();
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
+164
View File
@@ -0,0 +1,164 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.7.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Isolation_Mode" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 720 720" style="enable-background:new 0 0 720 720;" xml:space="preserve">
<style type="text/css">
.st0{fill:#E01F2F;}
</style>
<g>
<path class="st0" d="M631.71,544.4c-2.42-0.54-7.34-1.83-13.64-4.16c0.33,0.12,0.65,0.26,0.97,0.39
C624.37,542.61,628.45,543.68,631.71,544.4z"/>
<path class="st0" d="M633.4,544.75L633.4,544.75c-0.53-0.11-1.11-0.23-1.69-0.35C632.8,544.65,633.4,544.75,633.4,544.75z"/>
<path class="st0" d="M141.27,535.72C123.48,559.12,99,591.36,97.89,593.2c-1.18,1.97-7.99,13.54-20.39,18.46
c-17.32,5.6-21.78-2.86-21.78-2.86s-20.56,0.4-19.7,17.05h34.95v-0.02h33.31c0,0,0.58-14.58,15.16-17.49l60.92-48.82
C156.9,555.69,146.15,544.52,141.27,535.72z"/>
<g>
<path class="st0" d="M487.03,525.19l-76.29,72.09l72.21-76.16c-0.7-0.7-1.42-1.4-2.16-2.12c-9.15-8.9-17.68-13.07-26.83-13.07
c-10.91,0-22.71,5.71-37.09,13.4c2.02,14.66,2.11,33.84-7.43,46.78c-0.07,0.09-0.14,0.19-0.21,0.28
c-10.71,14.14-19.46,25.74-20.11,26.82c-1.19,1.98-8.11,13.75-20.72,18.61c-0.46,0.18-0.9,0.3-1.33,0.44
c-0.51,0.18-1.04,0.33-1.58,0.49c-0.7,0.19-1.4,0.37-2.07,0.49c-0.02,0-0.02,0-0.04,0c-11.13,2.07-16.38-4.44-16.38-4.44
s-20.56,0.4-19.7,17.05h34.95v-0.02h33.28c0,0,0.58-14.58,15.16-17.49c0,0,0,0,0.02-0.02c0.05-0.05,0.25-0.19,0.54-0.44l0.44-0.35
c6.99-5.78,49.59-41.09,73.3-66.94c3-3.27,5.69-6.39,7.96-9.27c-0.25-0.25-0.49-0.51-0.74-0.76
C490.59,528.89,488.89,527.1,487.03,525.19z"/>
<path class="st0" d="M384.77,207c0,0-0.02,20.99,7.18,27.11c7.2,6.15,22.41,1.46,32.15-9.76c11.84-13.64,18.61,5.09,18.61,5.09
c0.37-14.56-12.78-15.8-12.78-15.8c14.72-7.18,20.65,8.89,20.65,8.89c-0.67-7.2-4.21-15.58-16.17-15.56
c-16.51,0.04-18.79,17.6-33.3,20.07C386.6,229.51,384.77,207,384.77,207z"/>
<path class="st0" d="M681.85,338.39c-2.6-12.08-7.18-23.27-13-33.56c-5.83-10.29-13.01-19.69-21.41-28.04
c-8.38-8.34-18.05-15.77-29.27-21.3c-11.13-5.51-23.25-8.78-35.14-10.08c-11.92-1.3-23.62-0.74-34.79,0.84
c-11.15,1.6-21.88,4.23-31.96,7.08c-2.55,0.72-4.85,1.35-7.64,2.23c-1.35,0.42-2.67,0.79-4.04,1.26l-4.07,1.4
c-5.44,1.98-11.05,4.44-16.19,7.24c-1.32,0.68-2.53,1.39-3.76,2.09c-1.25,0.7-2.37,1.39-3.53,2.07l-6.46,3.88
c-4.11,2.46-8.08,4.64-12.19,6.71c-4.16,2.02-8.38,4-12.78,5.81l-3.28,1.37l-3.49,1.4l-6.92,2.74
c-9.15,3.56-18.16,6.76-27.22,9.32c-9.04,2.58-18.12,4.51-27.2,5.64c-9.08,1.12-18.14,1.4-27.13,0.74
c-1.12-0.09-2.25-0.14-3.37-0.23l-3.34-0.42l-3.35-0.42c-0.53-0.05-1.18-0.18-1.79-0.26l-1.81-0.28
c-4.78-0.77-9.4-1.62-13.96-2.63c-9.11-2.02-17.91-4.69-25.94-8.2c-8.03-3.51-15.24-7.92-20.67-13.21
c-5.46-5.3-8.94-11.31-10.55-18.25c-1.6-7.01-1.3-15.05,0.76-22.79c1-3.88,2.42-7.69,4.23-11.31c1.79-3.64,3.93-7.09,6.43-10.24
c2.48-3.18,5.25-6.08,8.31-8.57c3.04-2.51,6.32-4.69,9.82-6.39c1.72-0.91,3.58-1.51,5.36-2.28c1.86-0.53,3.69-1.23,5.62-1.56
c3.81-0.86,7.76-1.32,11.82-1.26c0.02,0,0.04,0,0.05,0c2.83,0.02,5.62,0.49,8.34,1.21c1.46,0.39,2.97,0.74,4.5,1.04
c3.93,0.77,7.39,1.58,10.47,2.44c0.23,0.09,0.46,0.18,0.68,0.26l0.04-0.07c3,0.84,5.62,1.72,7.87,2.58v-0.11
c0.86,0.37,1.69,0.76,2.46,1.12c0.02,0,0.02,0,0.04,0.02c10.78,5.22,12.05,9.69,12.05,9.69c0.04,0.09,0.09,0.12,0.12,0.21
c2.05,2.79,3.48,5.57,4.95,7.88c5.87,8.99,8.34,11.03,8.34,11.03l1.86-3.83c-7.55-8.34-9.78-32.65-9.78-32.65
s15.95-3.3,23.13,0.12c6.95,3.35,0.95,16.12,0.6,16.74c5.65-2.35,11.43-13.79,11.43-13.79c4.74-2.69,3.53-11.17,3.53-11.17
c-18.95-22.83-44.53-26.03-65.87-22.14c-2.48,0.04-4.92,0.14-7.34,0.33c-4.5,0.33-9.01,0.4-13.5,0.11
c-1.18-0.07-2.35-0.12-3.53-0.16c-6.09-0.18-12.29,0.39-18.37,1.65c-3.07,0.54-6.04,1.51-9.04,2.39
c-2.92,1.11-5.88,2.12-8.68,3.51c-5.65,2.65-10.94,6.02-15.77,9.87c-4.85,3.85-9.2,8.24-13.03,13c-3.86,4.74-7.18,9.89-9.96,15.3
c-2.79,5.41-5.04,11.1-6.67,17c-3.28,11.78-4.27,24.53-1.62,37.37c1.39,6.41,3.69,12.78,6.92,18.56
c3.21,5.8,7.27,11.01,11.73,15.51c4.48,4.51,9.34,8.34,14.36,11.63c5.02,3.28,10.2,6.06,15.44,8.45
c10.47,4.78,21.13,8.1,31.77,10.62c5.3,1.26,10.64,2.32,15.86,3.23l1.93,0.33c0.65,0.11,1.23,0.23,1.97,0.32l4.18,0.6l4.2,0.58
c1.4,0.14,2.81,0.25,4.21,0.37c11.22,0.97,22.5,0.76,33.56-0.44c22.14-2.37,43.22-8.82,62.78-16.17l7.25-2.76l3.58-1.39l3.74-1.49
c4.99-1.97,9.99-4.21,14.96-6.55c4.95-2.41,9.9-5.06,14.54-7.74c4.65-2.69,8.78-5.22,12.57-7.16c3.79-1.98,7.52-3.55,11.61-4.99
l3.13-1.02c1.04-0.35,2.16-0.63,3.23-0.95c2.05-0.61,4.65-1.3,6.97-1.93c9.45-2.55,18.49-4.58,27.41-5.74
c8.9-1.12,17.61-1.39,25.83-0.39c8.22,1,15.93,3.25,22.93,6.81c6.94,3.51,13.54,8.6,19.44,14.61
c5.88,6.01,11.06,12.92,15.12,20.32c4.07,7.38,7.09,15.21,8.68,22.99c1.55,7.76,1.6,15.38,0.02,22.23
c-1.6,6.85-4.71,12.98-9.52,18.32c-4.74,5.36-11.08,9.92-18.28,13.4c-7.2,3.48-15.26,5.9-23.39,7.11
c-4.06,0.61-8.15,0.91-12.12,0.88c-1,0.02-1.97-0.05-2.95-0.09l-2.81-0.28c-1.91-0.07-3.65-0.51-5.44-0.81
c-0.91-0.11-1.74-0.4-2.6-0.65c-0.86-0.26-1.72-0.44-2.58-0.74c-1.69-0.68-3.39-1.18-5.11-2.05l-1.3-0.58l-0.65-0.3l-0.67-0.37
l-2.67-1.44c-0.9-0.49-1.86-1.11-2.79-1.63l-1.4-0.84l-1.49-0.95c-3.86-2.42-8.29-5.37-13.01-8.25l-3.65-2.18
c-1.21-0.72-2.56-1.42-3.85-2.12c-2.55-1.44-5.36-2.72-8.1-4.02c-0.68-0.33-1.39-0.63-2.11-0.91l-2.14-0.86
c-1.44-0.56-2.86-1.18-4.32-1.67l-4.43-1.42c-1.48-0.47-2.95-0.95-4.46-1.26l-4.48-1.07c-0.76-0.18-1.49-0.35-2.25-0.51
l-2.27-0.39c-3-0.56-6.04-0.9-9.06-1.25c-6.08-0.54-12.15-0.67-18.18-0.37c-1.49,0.09-2.99,0.25-4.48,0.35l-2.25,0.19l-2.21,0.3
l-4.43,0.6l-4.36,0.81c-11.61,2.21-22.62,5.88-32.96,10.41c-5.18,2.25-10.19,4.76-15.08,7.39c-2,1.12-4.02,2.23-6.01,3.39
c-36.46,10.5-75.81,5.09-107.56-16.07c-0.84-0.56-1.72-1.16-2.6-1.76c-1.09-0.09-2.04-0.25-3.14-0.35
c-3.74-0.49-7.38-0.65-11.06-0.65c-4.23,0-8.38,0.4-12.42,1.09l-2.9,0.51l-2.99,0.74c-0.95,0.19-1.84,0.49-2.84,0.76
c-0.95,0.35-1.93,0.54-2.79,0.95c-3.85,1.3-7.48,3.09-11.12,4.99c2.9-2.95,6.18-5.55,9.87-7.57c3.74-2,7.64-3.76,11.71-4.85
c4.11-1.14,8.39-1.84,12.57-2.09c2.9-0.16,5.69-0.16,8.62,0c-6.22-7.73-19.79-23.18-31.14-27.08c0,0-39.74-30.17-52.4-39.72
c-12.71-9.68-27.17-27.13-24.94-32.86c0,0,9.57-13.96,17.89-16.54c8.27-2.7,10.47-12.28,10.47-12.28s-12.28,3.49-27.57,3.93
s-22.23,4.39-22.23,4.39s-8.78-7.04-18.81-8.34c0,0,0.84-30.15,40.64-49.4c39.78-19.18,86.49-44.5,104.42-98.2
c-1.49,2.35-24.97,39.6-53.28,50.1c-29.26,10.92-66.43,23.18-78.2,30.64c-11.85,7.43-34.56,32.35-47.7,79.13l-49.31,57.72
c0,0,2.58,6.97,6.94,6.53c0,0,1.3,9.57,10.01,5.62c0,0,2.23,10.08,0.46,14.01c-1.76,3.95,4.43,16.16,12.66,18.35l-0.79-4.79
c0,0,2.16,5.23,9.57,7.43c0,0-7.83-10.06-4.85-19.69c2.56-7.92,1.53-11.77,1.05-12.91l3.79,7.67c0,0,1.28-10.47-4.04-18.68
c1.93,2.49,11.36,12.92,31.26,5.94c1.79-1.62,3.49-3.28,5.04-4.99l2.18-2.53c0.76-0.81,1.32-1.76,2-2.65
c1.39-1.7,2.49-3.58,3.83-5.43c0.6-0.95,1.16-1.93,1.7-2.95c0.6-0.95,1.19-1.93,1.9-2.88c1.11-2,2.25-3.99,3.6-6.04
c-0.16,2.39-0.49,4.74-0.9,7.08c-0.39,1.19-0.65,2.35-0.98,3.49l-0.65,1.69c-0.21,0.61-0.3,1.16-0.6,1.7
c-0.49,1.09-0.98,2.23-1.55,3.28c-0.49,1.14-1.09,2.12-1.84,3.2c-0.6,0.98-1.23,2.09-1.93,3.04l-2.35,2.83
c-1.69,1.84-3.58,3.6-5.57,5.15c-0.46,4.23-1.7,10.22-4.39,15.73c-0.39,0.91-0.79,1.76-1.14,2.55c1.19-1,24.15-19.55,60.08-3.06
l1.9,0.79l-0.98,1.84l-2.65,4.58c4.79-1.14,9.68-1.19,14.47-0.11c6.41,1.55,11.77,6.09,15.3,11.27c3.69,5.2,5.74,11.22,7.48,17
c0.84,2.9,1.79,5.83,3.42,8.24c1.65,2.44,4.39,4.04,7.48,4.5c-3.09-0.05-6.23-1.35-8.18-3.9c-2.05-2.49-3.2-5.43-4.34-8.24
c-2.2-5.72-4.39-11.41-8.1-16.03c-3.58-4.6-8.38-8.24-13.91-9.54c-5.53-1.3-11.66-0.49-16.74,1.7l-4.95,2.09l2.55-4.74l3.79-6.83
c-7.43-3.04-15.4-4.64-23.32-3.9c-8.62,0.76-16.95,3.99-24.43,8.57c-0.05,0.05-0.11,0.09-0.16,0.16
c-9.71,5.83-17.4,14.45-22.93,24.32c-6.08,10.82-13.91,25.13-19.14,35.6c-9.68,19.19-18.35,22.74-18,31.45
c0.46,8.73-4.34,38.88-7.78,45.03c0,0,0.79,9.68,10.87,8.71c0,0,0,7.48,10.01,6.57c0,0-6.13-9.17,2.69-13.56
c8.78-4.34,14.4-0.39,16.56,4.79c2.2,5.27-4.79,12.85-10.22,8.82l-2.44-3.99c0,0-6.97,11.36,6.53,14.42c0,0,1.35,7.92,15.35,7.48
c0,0-7.87-3.93-1.69-10.08c0,0,10.41-3.93,8.32-23.62c0,0,0.25-1.4,1.19-2.25c0.14-1.88,0.32-3.78,0.65-5.62
c0.6-3.99,1.33-7.88,3.23-11.82c0.86-1.93,2.05-3.99,4.25-5.57l0.65-0.46l0.54-0.35c0.3-0.25,0.65-0.46,1.04-0.79
c0.56-0.49,1.35-1.05,1.84-1.69c2.49-2.55,4.69-5.58,6.48-8.87c1.98-3.2,3.69-6.69,5.18-10.19c-0.3,3.9-1.28,7.64-2.6,11.38
c-1.28,3.74-3.04,7.38-5.62,10.66c-0.65,0.84-1.35,1.63-2.11,2.39c-0.39,0.39-0.79,0.68-1.23,1.11l-0.6,0.49l-0.35,0.25
l-0.19,0.16c-0.81,0.74-1.62,2.12-2.25,3.74c-1.25,3.14-2.05,6.78-2.55,10.41c-0.35,2.2-0.6,4.34-0.79,6.48
c0.76,0.7,1.49,1.6,2.35,2.74c7.13,9.78,0.79,25.18,23.72,36.46c-0.84-2.09-1.69-4.28-2.42-6.48c-2.3-7.08-3.85-14.56-4.23-22.02
c-0.26-3.79-0.21-7.57,0-11.38c0.19-3.74,0.79-7.48,1.39-11.22c0.44-1.84,0.74-3.74,1.19-5.53l0.68-2.79l0.81-2.69
c0.54-1.79,1.19-3.6,1.88-5.37l0.79-2.55c0.37-0.79,0.56-1.6,0.81-2.49c1.93-6.73,2.83-13.77,1.9-20.63
c-0.9-6.78-3.49-13.4-7.78-18.79c-0.98-1.35-2.14-2.65-3.42-3.79c-1.25-1.14-2.65-2.16-4.09-3.09c-1.46-0.9-2.99-1.6-4.64-2.14
c-1.55-0.51-3.2-0.79-4.9-0.84c-3.55-0.16-6.94,0.49-10.03,2.35c-1.49,0.9-2.9,2.04-4.04,3.39c-1.25,1.3-2.35,2.84-3.06,4.48
c0.91-3.53,3.06-6.88,6.2-9.27c2.93-2.42,6.92-3.74,10.85-3.93c1.95-0.09,3.93,0,5.9,0.46c1.93,0.46,3.88,1.11,5.62,2.09
c1.76,0.84,3.44,2,4.93,3.2c1.55,1.23,2.99,2.6,4.28,4.04c2.65,2.95,4.83,6.29,6.34,9.87c1.69,3.58,2.9,7.38,3.34,11.26
c1.14,7.78,0.39,15.66-1.95,23.04c-0.25,0.9-0.54,1.83-0.84,2.74l-0.98,2.65c-0.61,1.7-1.25,3.39-1.76,5.09
c-1.16,3.39-2.09,6.88-2.9,10.38c-0.65,3.49-1.39,7.02-1.74,10.62c-0.3,3.53-0.6,7.18-0.49,10.76c0,3.6,0.19,7.18,0.54,10.76
c0.39,3.6,1.09,7.13,1.83,10.71c0.65,2.79,1.51,5.57,2.35,8.32c0,0.05,0.05,0.05,0.05,0.05s0,0.05,0.05,0.16
c1.33,4.13,3.09,8.11,5.13,11.98c2.79,3.88,6.94,8.08,13.26,11.56c6.34,3.42,14.82,6.27,26.18,7.32
c41.97,3.93,41.06,13.12,46.77,20.55c0,0,3.93-15.26-4.79-22.74c0,0,6.08-0.46,21.39,14.86c15.3,15.31,25.41,7.88,25.41,7.88
s-11.45,1.3-16.6-7.88c-5.27-9.17-7.52-30.56-14.51-34.93v-1.79c-1.09-1.25-2.2-2.55-3.35-3.9c-2.55-3.14-4.83-6.43-7.13-9.87
c-2.12-3.39-4.23-6.92-5.83-10.85c3.49,2.39,6.53,5.2,9.48,8.01c2.35,2.41,4.53,4.85,6.73,7.38c2.2,2.6,4.28,5.23,6.23,8.04
c3.14,4.53,5.9,9.11,8.13,14.36h0.05c13.72,17.44,32.15,43.62,28.57,55.07c-1.55,4.74-3.79,7.64-6.18,9.38
c-6.78,4.88-15.26,0.65-15.26,0.65s-20.58,0.44-19.69,17.05h62.5c0,0,7.43-10.52,16.26-13.96c8.62-3.51-19.37-72.54-19.74-73.48
c0.67,0.39,26.39,15.45,73.86-6.53c48.12-22.28,72.19-48.89,104.89-17.03c32.75,31.91,21.83,34.09,121.54,53.79
c0,0,34.1,13.12,19.65,38.88c0,0,0.11,0.44,0,1.09v-1.09c-10.22,9.15-16.4,1.3-16.4,1.3s-20.55,0.44-19.63,17.05h67.59
c0,0-1.84-10.52,7.32-13.96c9.24-3.53-10.06-27.97-16.16-50.29c-4.58-16.89-4.53-14.75-15.35-16.86c0,0-0.02,0-0.04-0.02
c-0.02,0-0.04-0.02-0.07-0.04c-0.05,0-0.16,0-0.25-0.05h-0.25c-0.25-0.05-0.6-0.16-1.09-0.25h-0.05c-0.21-0.05-0.4-0.09-0.61-0.16
c-0.11-0.02-0.19-0.05-0.3-0.07s-0.23-0.05-0.35-0.07c-0.05,0-0.11,0-0.19-0.05c-0.11,0-0.21-0.05-0.25-0.05
c-0.05-0.04-0.09-0.04-0.16-0.05c-0.02,0-0.04-0.02-0.05-0.02s-0.04-0.02-0.04-0.02c-0.04-0.02-0.04-0.02-0.07-0.04
c-0.05,0-0.09-0.05-0.19-0.05h-0.11c-0.09-0.05-0.19-0.05-0.3-0.05c-0.7-0.21-1.55-0.4-2.41-0.65c-0.09,0-0.16-0.05-0.25-0.05
c-0.11,0-0.16-0.05-0.25-0.09c-0.26-0.09-0.53-0.18-0.81-0.26c-0.19-0.05-0.39-0.11-0.56-0.18c-0.18-0.07-0.37-0.11-0.54-0.18
c-0.18-0.05-0.37-0.11-0.54-0.18c-0.3-0.11-0.6-0.19-0.91-0.26c-1.14-0.4-2.46-0.79-3.74-1.3c-0.16-0.11-0.33-0.16-0.49-0.25
c0-0.02-0.02-0.02-0.02-0.02h-0.02c-0.11-0.04-0.18-0.05-0.23-0.09c-0.04-0.02-0.07-0.04-0.11-0.05c0,0,0,0-0.02-0.02
s-0.05-0.04-0.09-0.05c-8.9-3.34-20.58-8.85-31.89-17.65c0,0-0.04-0.04-0.11-0.09c-3.9-3.04-7.76-6.44-11.43-10.29
c-0.04-0.04-0.07-0.07-0.11-0.11c-13.26-12.8-34.72-36.3-47.31-64.59c-0.35-0.79-0.74-1.58-1.12-2.35c0-0.02-0.02-0.02-0.02-0.02
c-3.25-6.37-7.92-12.15-13.49-17.37c-0.3-0.28-0.6-0.56-0.9-0.83c0.02,0,0.04,0.02,0.05,0.02c-4.79-4.69-10.1-8.9-15.61-12.66
l2.81,0.84c0.93,0.3,1.84,0.7,2.77,1.05l1.39,0.53c0.46,0.16,0.93,0.35,1.39,0.58c1.83,0.88,3.67,1.63,5.53,2.69
c0.93,0.51,1.84,0.95,2.81,1.51l2.88,1.69c0.95,0.53,2,1.21,2.99,1.83l3.09,1.91l3.21,2.02l3.44,2.12
c9.15,5.71,21.35,11.94,34.28,14.31l4.81,0.83c1.62,0.32,3.18,0.35,4.78,0.51l4.76,0.37c1.56,0.04,3.13,0.11,4.65,0.07
c6.16-0.02,12.2-0.56,18.16-1.53c11.87-1.93,23.43-5.51,34.4-10.98c5.5-2.72,10.82-5.97,15.89-9.75
c5.08-3.76,9.85-8.15,14.14-13.1c4.3-4.93,8.08-10.5,11.08-16.47c3-5.99,5.25-12.35,6.64-18.83
C684.92,363.52,684.41,350.44,681.85,338.39z M379.22,175.86l15,10.55l-23.46-8.52L379.22,175.86z M85.99,279.4l11.61-13.42h8.43
L85.99,279.4z M150.4,270.47l5.6-12.15l29.31-10.52l-23.99,11.36L150.4,270.47z M70.99,449.54l11.61-13.36h8.52L70.99,449.54z
M170.54,512c-3.69-5.58-5.64-12.36-6.04-19.11c-0.25-3.34-0.05-6.73,0.35-10.06c0.25-0.86,0.35-1.7,0.54-2.6
c0.19-1.05,0.4-1.6,0.56-2.44c0.44-1.49,0.68-2.99,0.98-4.53c1.14-6.18,1.69-12.52,1.79-18.81c0.32-12.61-1.39-25.32-4.64-37.48
c-0.84-3.09-1.84-6.04-2.99-8.92c-1.14-2.95-2.6-5.74-4.23-8.32c-1.7-2.65-3.69-5.08-6.13-7.02c-1.25-0.95-2.53-1.81-3.99-2.49
c-1.44-0.65-2.95-1.16-4.53-1.25c3.2-0.46,6.59,0.65,9.48,2.35c1.74,0.98,3.39,2.23,4.79,3.6c0.65-1.46,1.19-2.9,1.44-4.41
c0.35-1.53,0.44-3.09,0.44-4.64c-0.09-3.04-0.79-6.04-1.93-8.87c-1.09-2.84-2.6-5.58-4.34-8.18c-1.4-2.18-2.95-4.34-4.65-6.27
l-2.48,0.81c-1.25,0.46-2.55,0.95-3.83,1.39c-1.26,0.54-2.46,1.09-3.69,1.65l-1.7,0.98c-0.6,0.3-1.19,0.6-1.79,1
c-2.25,1.39-4.28,3.04-5.94,5.09c-0.51,0.54-0.84,1.09-1.16,1.63c-0.25,0.3-0.46,0.6-0.6,0.9c-0.09,0.3-0.35,0.6-0.44,0.91
c-0.54,1.23-0.9,2.53-1.05,3.99c-0.3-1.4-0.3-2.9-0.11-4.36c0.11-0.68,0.35-1.44,0.54-2.12c0.35-0.65,0.6-1.46,0.95-2.05
c0.74-1.39,1.55-2.53,2.6-3.74l1.44-1.63c0.61-0.49,1.16-0.95,1.76-1.46c0.54-0.54,1.19-0.93,1.74-1.33l1.84-1.3
c1.25-0.65,2.55-1.46,3.83-2.05c1.39-0.6,2.69-1.25,4.04-1.76l4.09-1.44l1.35-0.46l1,1.04c1.28,1.35,2.25,2.65,3.28,3.99
c1.05,1.35,2,2.74,2.79,4.2c1.79,2.88,3.2,6.02,4.13,9.41c0.4,1.6,0.79,3.28,0.9,5.04c0.16,1.74,0.05,3.49-0.16,5.18
c-0.35,1.69-0.79,3.34-1.49,4.88c-0.81,1.46-1.7,2.84-2.76,4c0.98,0.95,1.84,1.9,2.76,2.97c2.12,2.6,3.88,5.44,5.43,8.43
c1.55,2.95,2.84,6.09,3.88,9.17c2.2,6.29,3.69,12.71,4.76,19.25c0.95,6.53,1.33,13.15,1.14,19.79
c-0.16,6.62-0.98,13.29-2.46,19.79c-0.35,1.6-0.9,3.2-1.39,4.83c-0.19,0.76-0.54,1.69-0.68,2.2c-0.19,0.68-0.35,1.49-0.46,2.2
c-0.65,2.99-1,6.11-1,9.22c0,3.09,0.35,6.18,1.05,9.18c0.65,3.09,1.63,5.99,3.09,8.82c1.44,2.74,3.28,5.34,5.43,7.67
c2.11,2.34,4.53,4.5,7.18,6.32C179.46,522.03,174.23,517.67,170.54,512z M207.58,458.21c0.25,2.49,0.76,4.97,1.35,7.57
c0.65,2.48,1.6,4.93,2.53,7.48c-2.04-1.79-3.79-4.04-5.18-6.43c-1.35-2.44-2.41-5.02-3.2-7.81c-0.68-2.69-1.04-5.5-1.23-8.29
c-0.16-2.83,0-5.62,0.35-8.36c0.6-5.6,2.2-10.98,4.34-16c2.2-4.99,4.83-9.78,8.53-13.91c-1.49,5.18-3.04,10.26-4.25,15.4
c-1.23,5.04-2.18,10.12-2.9,15.21C207.32,448.1,207.18,453.17,207.58,458.21z M601.46,548.79c5.23,0.49,10.36,1.46,15.44,2.9
c2.49,0.76,4.99,1.65,7.34,2.79c2.39,1.19,4.69,2.6,6.73,4.43l1.46,1.55c0.39,0.49,0.74,0.98,1.19,1.49
c0.79,1.05,1.65,2.11,2.46,3.2c1.49,2.09,3.04,4.23,4.37,6.5c2.65,4.48,5.08,9.11,7.18,14l-0.19,0.11
c-3.2-4.2-6.09-8.53-8.97-12.82l-4.36-6.48c-0.65-1.09-1.39-2.14-2.12-3.25c-0.4-0.49-0.81-1.04-1.11-1.6l-1.14-1.35
c-1.84-1.74-3.9-3.23-6.13-4.37c-2.3-1.25-4.64-2.23-7.13-3.09c-4.83-1.74-9.87-2.93-15.07-3.74L601.46,548.79z M438.98,462.85
c-1.6,0.95-3.04,2.09-4.39,3.28c-1.35,1.14-2.6,2.55-3.69,3.99c-0.6,0.65-1.05,1.39-1.55,2.16c-0.46,0.76-0.9,1.55-1.19,2.23v0.05
l-0.16,0.21c-2.55,4.07-5.34,8.08-8.59,11.68c-1.39,1.93-3.18,3.64-4.88,5.43c-0.84,0.84-1.69,1.6-2.6,2.49
c-0.95,0.79-1.74,1.63-2.74,2.39c-7.39,6.34-15.72,11.52-24.53,15.51c-8.78,4.04-18.16,6.78-27.57,8.52
c-4.79,0.84-9.48,1.46-14.29,1.65c-2.46,0.16-4.85,0.19-7.24,0.16c-2.35-0.05-4.69-0.16-7.04-0.46
c9.41-1.05,18.65-2.84,27.61-5.34c4.55-1.19,8.92-2.69,13.36-4.28c4.28-1.6,8.62-3.34,12.77-5.39
c8.32-3.99,16.24-8.82,23.37-14.59c7.24-5.72,13.66-12.42,19.44-19.74c1.05-1.79,2.25-3.2,3.64-4.55
c1.35-1.44,2.95-2.69,4.58-3.74c1.65-1.09,3.34-1.98,5.15-2.63c1.79-0.76,3.74-1.16,5.57-1.51c-0.84,0.4-1.79,0.7-2.58,1.19
C440.67,461.95,439.77,462.35,438.98,462.85z M507.47,503.47l2.49,3.6c0.84,1.11,1.6,2.28,2.49,3.39l2.65,3.34l1.39,1.65
l1.39,1.49c1.02,1.05,2,2.05,2.99,3.09l3.09,2.84c1.09,1.05,2.2,1.9,3.28,2.79c1.09,0.9,2.2,1.79,3.39,2.63
c4.53,3.39,9.43,6.39,14.51,8.94c5.18,2.55,10.57,4.74,16.16,6.18l-0.16,0.54c-5.8-0.98-11.38-2.79-16.7-5.09
c-5.34-2.23-10.57-4.97-15.44-8.22c-1.32-0.68-2.49-1.63-3.71-2.49c-1.19-0.9-2.34-1.69-3.48-2.67l-3.35-2.84
c-1.09-0.93-2.2-1.98-3.28-2.99l-1.55-1.53l-1.49-1.65l-3.04-3.28c-0.95-1.14-1.81-2.28-2.69-3.49l-2.69-3.42l-1.3-1.65
l-1.39-1.55c-0.86-1.09-1.84-1.98-2.79-2.99c-2.05-1.83-4.18-3.64-6.43-5.18c-2.3-1.62-4.58-2.95-7.08-4.23
c-2.39-1.25-4.88-2.46-7.52-3.39c-10.27-4.04-21.58-5.9-33.12-6.2l-0.09-0.6c5.69-1.55,11.63-2.05,17.54-1.81
c2.93,0.11,5.99,0.46,8.87,0.91c2.99,0.49,5.94,1.3,8.82,2.2c1.4,0.46,2.79,0.98,4.2,1.55c1.39,0.6,2.79,1.23,4.14,1.98
c1.33,0.65,2.58,1.46,3.93,2.3c0.6,0.39,1.25,0.84,1.9,1.23c0.61,0.51,1.21,0.95,1.81,1.4C500.04,494,504.13,498.55,507.47,503.47
z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

+370 -9
View File
@@ -1,42 +1,403 @@
import { makeVar } from '@apollo/client';
import React from 'react';
import {restartWebsockets} from "./index";
import {snackActions} from "./components/utilities/Snackbar";
import {mergeRefreshedUser} from "./userState";
export const meState = makeVar({loggedIn:false, user: null, access_token: null, refresh_token: null});
export const menuOpen = makeVar(false);
export const alertCount = makeVar(0);
export const taskTimestampDisplayFieldOptions = [
{
name: "timestamp",
display: "Latest Timestamp for anything task related"
},
{
name: "status_timestamp_preprocessing",
display: "When Operator Submitted Task"
},
{
name: "status_timestamp_processing",
display: "When Agent Picked up Task",
}
]
export const taskingDisplayFieldOptions = [
{
name: "timestamp",
display: "Timestamp",
description: "Show the configured task timestamp."
},
{
name: "task",
display: "Task number",
description: "Show the T-number link for each task."
},
{
name: "username",
display: "Username",
description: "Show the operator that issued the task."
},
{
name: "callback",
display: "Callback number",
description: "Show the C-number link for the task callback."
},
{
name: "host",
display: "Host",
description: "Show the callback host."
},
{
name: "ip",
display: "IP address",
description: "Show the callback primary IP."
},
{
name: "groups",
display: "Callback groups",
description: "Show the callback's tree groups."
},
{
name: "payload_type",
display: "Payload type",
description: "Show the task payload type."
},
];
export const defaultTaskingDisplayFields = ["timestamp", "task", "username", "callback", "payload_type"];
export const normalizeTaskingDisplayFields = (fields) => {
if(!Array.isArray(fields)){
return [...defaultTaskingDisplayFields];
}
const validFieldNames = taskingDisplayFieldOptions.map((option) => option.name);
return fields.reduce( (prev, fieldName) => {
if(validFieldNames.includes(fieldName) && !prev.includes(fieldName)){
return [...prev, fieldName];
}
return prev;
}, []);
}
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", "Chat",
].sort();
export const operatorSettingDefaults = {
navBarOpen: false,
fontFamily: 'Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
showMedia: true,
showOPSECBypassUsername: false,
taskingDisplayFields: defaultTaskingDisplayFields,
useDisplayParamsForCLIHistory: true,
interactType: "interactSplit",
taskTimestampDisplayField: "timestamp",
callbacks_table_columns: ["Interact", "Host", "Domain", "User", "Description", "Last Checkin", "Agent", "IP", "PID"],
callbacks_table_filters: {},
autoTaskLsOnEmptyDirectories: false,
hideBrowserTasking: false,
hideTaskingContext: false,
taskingContextFields: ["impersonation_context", "cwd"],
["experiment-responseStreamLimit"]: 200,
chatSelectedChannelID: 0,
virtualizedTablePadding: 8,
sideShortcuts: defaultShortcuts,
palette: {
primary: {
dark: "#7dd3fc",
light: "#2563eb",
},
error: {
dark: '#fb7185',
light: '#dc2626'
},
success: {
dark: '#34d399',
light: '#15803d',
},
secondary: {
dark: '#94a3b8',
light: '#64748b'
},
info: {
dark: '#22d3ee',
light: '#0891b2'
},
warning: {
dark: "#fbbf24",
light: "#b45309",
},
background: {
dark: '#0b0f14',
light: '#f6f8fb'
},
paper: {
dark: '#111820',
light: '#ffffff'
},
surfaceRaised: {
dark: '#16212b',
light: '#ffffff'
},
surfaceMuted: {
dark: '#0f1720',
light: '#fafbfc'
},
tableHeader: {
dark: '#17212c',
light: '#e8eef5'
},
tableHover: {
dark: "#1e2a36",
light: "#eaf1f8",
},
pageHeader: {
dark: '#111a23',
light: '#ffffff'
},
pageHeaderText: {
dark: "#e7edf5",
light: "#0f172a",
},
chatMessageOperatorBackground: {
dark: "#111820",
light: "#ffffff",
},
chatMessageSelfBackground: {
dark: "#142033",
light: "#f8fbff",
},
chatMessageSystemBackground: {
dark: "#2a2116",
light: "#fff7ed",
},
chatMarkdownSurfaceBackground: {
dark: "#0b1118",
light: "#f5f5f5",
},
chatMarkdownSurfaceStrongBackground: {
dark: "#1f2a37",
light: "#e2e8f0",
},
sectionHeaderAccent: {
dark: "#7dd3fc",
light: "#b3cbff",
},
sectionHeaderGradientStart: {
dark: "#153049",
light: "#dbeafe",
},
sectionHeaderGradientMiddle: {
dark: "#111f2e",
light: "#eef6ff",
},
sectionHeaderGradientEnd: {
dark: "#111820",
light: "#ffffff",
},
subtleAccentGradientStart: {
dark: "#132338",
light: "#eef6ff",
},
subtleAccentGradientEnd: {
dark: "#111820",
light: "#ffffff",
},
graphGroupColor: {
dark: "#24384a",
light: "#d9e6f2",
},
chartSeries1: {
dark: "#38bdf8",
light: "#0284c7",
},
chartSeries2: {
dark: "#34d399",
light: "#16a34a",
},
chartSeries3: {
dark: "#fbbf24",
light: "#ca8a04",
},
chartSeries4: {
dark: "#fb7185",
light: "#e11d48",
},
chartSeries5: {
dark: "#a78bfa",
light: "#7c3aed",
},
chartSeries6: {
dark: "#2dd4bf",
light: "#0d9488",
},
chartSeries7: {
dark: "#f472b6",
light: "#db2777",
},
chartSeries8: {
dark: "#f97316",
light: "#ea580c",
},
chartSeries9: {
dark: "#818cf8",
light: "#4f46e5",
},
chartSeries10: {
dark: "#a3e635",
light: "#65a30d",
},
text: {
dark: "#e6edf3",
light: "#111827",
},
textSecondary: {
dark: "#a7b2c2",
light: "#526174",
},
textDisabled: {
dark: "#6f7c8d",
light: "#93a1b2",
},
selectedCallbackColor: {
dark: '#123a5a',
light: '#dbeafe',
},
selectedCallbackHierarchyColor: {
dark: '#172d49',
light: '#edf6ff',
},
backgroundImage: {
dark: null,
light: null
},
navBarIcons: {
dark: '#dce8f8',
light: '#ffffff'
},
navBarText: {
dark: '#dce8f8',
light: '#ffffff'
},
navBarColor: {
dark: "#1a283d",
light: "#5c73a5",
},
navBarBottomColor: {
dark: "#0c131c",
light: "#1f2937",
},
taskPromptTextColor: {
dark: '#9fb3c8',
light: '#64748b'
},
taskPromptCommandTextColor: {
dark: "#f8fafc",
light: "#0f172a",
},
taskContextColor: {
dark: "#14324f",
light: "#dbeafe",
},
taskContextImpersonationColor: {
dark: "#4a1824",
light: "#fee2e2",
},
taskContextExtraColor: {
dark: "#123a35",
light: "#dcfce7",
},
emptyFolderColor: {
dark: '#8997aa',
light: '#64748b'
},
folderColor: {
dark: '#f2c86b',
light: '#d6a23f'
},
outputBackgroundColor: {
dark: '#070b10',
light: '#f8fafc'
},
outputTextColor: {
dark: '#e6edf3',
light: '#111827',
},
borderColor: {
dark: "#263241",
light: "#d5dee9"
}
},
}
export const mePreferences = makeVar(operatorSettingDefaults);
export const successfulLogin = (data) => {
localStorage.setItem("access_token", data.access_token);
localStorage.setItem("refresh_token", data.refresh_token);
localStorage.setItem("user", JSON.stringify(data.user));
let now = new Date();
let serverNow = new Date(data.user.current_utc_time);
const difference = (serverNow.getTime() - now.getTime());
let me = {...data.user};
me.server_skew = difference;
me.login_time = now;
meState({
loggedIn: true,
...data
access_token: data.access_token,
refresh_token: data.refresh_token,
user: {
...me
}
});
localStorage.setItem("user", JSON.stringify(me));
restartWebsockets();
}
export const successfulRefresh = (data) => {
localStorage.setItem("access_token", data.access_token);
localStorage.setItem("refresh_token", data.refresh_token);
let now = new Date();
let serverNow = new Date(data.user.current_utc_time);
const difference = (serverNow.getTime() - now.getTime()) ;
let me = mergeRefreshedUser(meState().user, data.user);
me.server_skew = difference;
me.login_time = now;
meState({
loggedIn: true,
access_token: localStorage.getItem("access_token"),
...meState()
access_token: data.access_token,
refresh_token: data.refresh_token,
user: {
...me
}
});
localStorage.setItem("user", JSON.stringify(me));
}
export const FailedRefresh = () =>{
export const FailedRefresh = (restart_websockets) =>{
const accessToken = localStorage.getItem("access_token");
if(accessToken){
fetch('/logout', {
method: 'POST',
headers: {'Authorization': `Bearer ${accessToken}`},
keepalive: true,
}).catch(() => {});
}
console.log("failed refresh");
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
localStorage.removeItem("user");
// retrieve all cookies
let Cookies = document.cookie.split(';');
// set past expiry to all cookies
for (let i = 0; i < Cookies.length; i++) {
document.cookie = Cookies[i] + "=; expires="+ new Date(0).toUTCString();
}
meState({
loggedIn: false,
access_token: null,
refresh_token: null,
user: null
});
mePreferences(operatorSettingDefaults);
snackActions.clearAll();
restartWebsockets();
}
if(restart_websockets){
restartWebsockets();
}
}
File diff suppressed because it is too large. Load diff
@@ -1,26 +1,12 @@
import React, {useEffect} from 'react';
import {gql, useSubscription} from '@apollo/client';
import {snackActions} from './utilities/Snackbar';
import {getSkewedNow} from "./utilities/Time";
//fromNow must be in ISO format for hasura/postgres stuff
//new Date().toISOString() will do it
/*
const subscribe_payloads = gql`
subscription EventFeedNotificationSubscription($fromNow: timestamp!, $operation_id: Int!) {
operationeventlog(limit: 1, where: {deleted: {_eq: false}, timestamp: {_gte: $fromNow}, operation_id: {_eq: $operation_id}}, order_by: {id: desc}) {
operator {
username
}
id
message
level
resolved
source
}
}
`;
*/
const subscribe_payloads = gql`
subscription EventFeedNotificationSubscription($fromNow: timestamp!) {
operationeventlog_stream(cursor: {initial_value: {timestamp: $fromNow}, ordering: ASC}, batch_size: 1, where: {deleted: {_eq: false}}) {
operator {
@@ -31,54 +17,53 @@ subscription EventFeedNotificationSubscription($fromNow: timestamp!, $operation_
level
resolved
source
warning
}
}
`;
export function EventFeedNotifications(props) {
const me = props.me;
const fromNow = React.useRef( (new Date()).toISOString() );
const { loading, error, data } = useSubscription(subscribe_payloads, {
variables: {fromNow: fromNow.current},
export function EventFeedNotifications() {
const [fromNow, setFromNow] = React.useState(getSkewedNow().toISOString());
//const fromNow = React.useRef( );
useSubscription(subscribe_payloads, {
variables: {fromNow: fromNow },
fetchPolicy: "no-cache",
shouldResubscribe: true,
onError: (errorData) => {
snackActions.warning("Failed to get event notifications");
console.log(errorData);
},
onData: ({data}) => {
if (data.data.operationeventlog_stream.length > 0){
if(data.data.operationeventlog_stream[0].level === "api" ||
data.data.operationeventlog_stream[0].level === "debug" ||
data.data.operationeventlog_stream[0].level === "agent"
){
return;
}
if(data.data.operationeventlog_stream[0].resolved){
return;
}
if(data.data.operationeventlog_stream[0].operator){
const message = data.data.operationeventlog_stream[0].operator.username + ":\n" + data.data.operationeventlog_stream[0].message;
if (data.data.operationeventlog_stream[0].warning) {
snackActions.warning(message, { autoClose: 2000});
} else {
snackActions.info(message, { autoClose: 2000});
}
}else {
if (data.data.operationeventlog_stream[0].warning) {
snackActions.warning(data.data.operationeventlog_stream[0].message, {autoClose: 3000});
} else {
snackActions.info(
<div style={{width: "100%"}}>{data.data.operationeventlog_stream[0].message}</div>,
{autoClose: 3000});
}
}
}
}
});
useEffect( () => {
//console.log(data, loading, error, fromNow.current);
if(!loading && !error && data && data.operationeventlog_stream.length > 0){
if(data.operationeventlog_stream[0].source === "debug" || data.operationeventlog_stream[0].level === "debug" ){
return;
}
if(data.operationeventlog_stream[0].resolved){
return;
}
if(data.operationeventlog_stream[0].operator){
const message = data.operationeventlog_stream[0].operator.username + ":\n" + data.operationeventlog_stream[0].message;
if (data.operationeventlog_stream[0].level === "warning") {
snackActions.warning(message, { autoClose: 2000});
} else {
snackActions.info(message, { autoClose: 2000});
}
}else {
if (data.operationeventlog_stream[0].level === "warning") {
snackActions.warning(data.operationeventlog_stream[0].message, {autoClose: 3000});
} else {
snackActions.info(
<div style={{width: "100%"}}>{data.operationeventlog_stream[0].message}</div>,
{autoClose: 3000});
}
}
}else if(error){
console.error(error);
snackActions.error("Mythic encountered an error getting operational event stream", {autoHideDuration: 2000});
}
}, [loading, data, error, me.user]);
return null;
return null
}
@@ -0,0 +1,291 @@
import React from 'react';
import {Box, Checkbox, InputAdornment, TextField, Typography} from '@mui/material';
import {MythicActionButton} from './MythicActionButton';
import SearchIcon from '@mui/icons-material/Search';
import {gql, useQuery} from "@apollo/client";
import {MythicChip} from './MythicChip';
export const defaultAPITokenScopes = ["*"];
export const apiTokenScopeDefinitionsQuery = gql`
query apiTokenScopeDefinitionsQuery {
apiTokenScopeDefinitions {
status
error
scopes {
name
display_name
description
resource
access
includes
}
grantable_wildcards
}
}
`;
export const normalizeAPITokenScopeSelection = (scopes = []) => {
const uniqueScopes = Array.from(new Set((scopes || []).filter((scope) => Boolean(scope)))).sort();
if(uniqueScopes.includes("*")){
return ["*"];
}
return uniqueScopes.reduce((prev, scope) => {
const resource = scope.endsWith(".*") ? scope.slice(0, -2) : scope.split(".")[0];
if(scope.endsWith(".*")){
return [
...prev.filter((selectedScope) => !selectedScope.startsWith(`${resource}.`)),
scope,
].sort();
}
if(prev.includes(`${resource}.*`)){
return prev;
}
return [...prev, scope].sort();
}, []);
};
export function APITokenScopeSelector({
selectedScopes = [],
onChange,
requiredScopes = [],
requiredScopeDescriptions = {},
fetchPolicy = "cache-first",
libraryMaxHeight,
className = "",
sx = {},
}) {
const [filter, setFilter] = React.useState("");
const normalizedSelectedScopes = React.useMemo(() => {
return normalizeAPITokenScopeSelection(selectedScopes);
}, [selectedScopes]);
const {data: scopeData, loading: scopeLoading, error: scopeQueryError} = useQuery(apiTokenScopeDefinitionsQuery, {
fetchPolicy,
});
const availableScopes = React.useMemo(() => {
if(scopeData?.apiTokenScopeDefinitions?.status !== "success"){
return [];
}
return scopeData?.apiTokenScopeDefinitions?.scopes || [];
}, [scopeData]);
const grantableWildcards = React.useMemo(() => {
return scopeData?.apiTokenScopeDefinitions?.grantable_wildcards || [];
}, [scopeData]);
const groupedScopes = React.useMemo(() => {
return availableScopes.reduce((prev, cur) => {
const resource = cur.resource || "other";
return {
...prev,
[resource]: [...(prev[resource] || []), cur],
};
}, {});
}, [availableScopes]);
const visibleResources = React.useMemo(() => {
const normalizedFilter = filter.toLowerCase();
return Object.keys(groupedScopes).sort().filter(resource => {
if(normalizedFilter === ""){
return true;
}
if(resource.toLowerCase().includes(normalizedFilter)){
return true;
}
return groupedScopes[resource].some(scope =>
scope.name.toLowerCase().includes(normalizedFilter) ||
(scope.display_name || "").toLowerCase().includes(normalizedFilter) ||
(scope.description || "").toLowerCase().includes(normalizedFilter)
);
});
}, [filter, groupedScopes]);
const visibleScopeCount = React.useMemo(() => {
return visibleResources.reduce((prev, resource) => prev + groupedScopes[resource].length, 0);
}, [groupedScopes, visibleResources]);
const scopeLoadFailed = scopeQueryError !== undefined || scopeData?.apiTokenScopeDefinitions?.status === "error";
const scopesUnavailable = scopeLoading || scopeLoadFailed;
const selectedScopesLabel = normalizedSelectedScopes.includes("*") ? "Full access selected" : `${normalizedSelectedScopes.length} selected`;
const fullAccessDisabled = !grantableWildcards.includes("*");
const emitScopes = (nextScopes) => {
if(onChange){
onChange(normalizeAPITokenScopeSelection(nextScopes));
}
}
const scopeIsSelected = (scope) => {
return normalizedSelectedScopes.includes(scope);
}
const toggleScope = (scope) => {
if(scope === "*"){
emitScopes(scopeIsSelected("*") ? [] : defaultAPITokenScopes);
return;
}
const resource = scope.endsWith(".*") ? scope.slice(0, -2) : scope.split(".")[0];
const withoutAll = normalizedSelectedScopes.filter(s => s !== "*");
const withoutResourceWildcard = withoutAll.filter(s => s !== `${resource}.*`);
if(scope.endsWith(".*")){
emitScopes(normalizedSelectedScopes.includes(scope) ? withoutResourceWildcard : [
...withoutResourceWildcard.filter(s => !s.startsWith(`${resource}.`)),
scope,
].sort());
return;
}
if(normalizedSelectedScopes.includes(scope)){
emitScopes(withoutResourceWildcard.filter(s => s !== scope));
return;
}
emitScopes([...withoutResourceWildcard, scope].sort());
}
const selectVisibleScopes = () => {
const visibleScopeNames = visibleResources.flatMap(resource => groupedScopes[resource].map(scope => scope.name));
emitScopes(Array.from(new Set([
...normalizedSelectedScopes.filter(scope => scope !== "*"),
...visibleScopeNames,
])).sort());
}
const clearScopes = () => {
emitScopes([]);
}
const librarySx = libraryMaxHeight === undefined ? undefined : {maxHeight: libraryMaxHeight};
return (
<Box className={className} sx={sx}>
<Box sx={{alignItems: "center", display: "flex", flexWrap: "wrap", gap: 0.75, mb: 1}}>
<MythicChip label={`${visibleScopeCount} visible`} />
<MythicChip label={selectedScopesLabel} />
<MythicActionButton disabled={scopesUnavailable || visibleScopeCount === 0 || scopeIsSelected("*")} size="small" onClick={selectVisibleScopes}>
Select Visible
</MythicActionButton>
<MythicActionButton disabled={normalizedSelectedScopes.length === 0} size="small" onClick={clearScopes}>
Clear
</MythicActionButton>
</Box>
{requiredScopes.length > 0 &&
<Box className="mythic-api-token-scope-card items-start flex gap-3 mythic-api-token-scope-card-selected min-w-0 rounded cursor-pointer bg-surface border-subtle text-primary mythic-tone-primary bg-tone-1 border-tone-3" sx={{mb: 1.25}}>
<Box className="mythic-api-token-scope-card-copy min-w-0 w-full">
<Typography className="mythic-api-token-scope-card-title text-sm font-800 leading-125 min-w-0 wrap-anywhere text-primary">Needed for this use</Typography>
{requiredScopes.map((scope) => (
<Typography key={`required-${scope}`} variant="caption" color="text.secondary" sx={{display: "block"}}>
{scope}: {requiredScopeDescriptions[scope] || "Required for this workflow."}
</Typography>
))}
</Box>
</Box>
}
<TextField
className="mythic-api-token-scope-search"
size="small"
fullWidth
value={filter}
onChange={(event) => setFilter(event.target.value)}
placeholder="Search scopes"
disabled={scopesUnavailable}
InputProps={{startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment>}}
/>
<Box className="mythic-api-token-scope-library flex flex-column gap-5 overflow-auto rounded bg-neutral-1 border-subtle" sx={librarySx}>
{scopesUnavailable &&
<Box className={`mythic-api-token-scope-state items-center flex justify-center rounded bg-surface-raised border-subtle text-muted text-center${scopeLoadFailed ? " mythic-api-token-scope-state-error mythic-tone-error bg-tone-1 border-tone-3 text-tone" : ""}`}>
<Typography variant="body2">
{scopeLoading ? "Loading scopes..." : (scopeQueryError?.message || scopeData?.apiTokenScopeDefinitions?.error || "Failed to load scopes")}
</Typography>
</Box>
}
{!scopesUnavailable &&
<>
<Box
className={`mythic-api-token-scope-card items-start flex gap-3 mythic-api-token-scope-card-full rounded cursor-pointer bg-surface border-subtle text-primary mythic-tone-warning bg-tone-1 border-tone-2${scopeIsSelected("*") ? " mythic-api-token-scope-card-selected mythic-tone-primary bg-tone-1 border-tone-3" : ""}${fullAccessDisabled ? " mythic-api-token-scope-card-disabled" : ""} min-w-0`}
component="label"
>
<Checkbox
disabled={fullAccessDisabled}
checked={scopeIsSelected("*")}
onChange={() => toggleScope("*")}
/>
<Box className="mythic-api-token-scope-card-copy min-w-0 w-full">
<Typography className="mythic-api-token-scope-card-title text-sm font-800 leading-125 min-w-0 wrap-anywhere text-primary">Full access (*)</Typography>
<Typography className="mythic-api-token-scope-card-description text-xs leading-135 wrap-anywhere text-muted">
Grants every current and future API scope available to this operator.
</Typography>
</Box>
</Box>
{visibleResources.length === 0 &&
<Box className="mythic-api-token-scope-state items-center flex justify-center rounded bg-surface-raised border-subtle text-muted text-center">
<Typography variant="body2">No scopes match your search.</Typography>
</Box>
}
{visibleResources.map(resource => {
const resourceWildcard = `${resource}.*`;
const canGrantResourceWildcard = grantableWildcards.includes(resourceWildcard);
const resourceScopes = [...groupedScopes[resource]].sort((a, b) => (a.access || "").localeCompare(b.access || ""));
const resourceWildcardSelected = scopeIsSelected("*") || scopeIsSelected(resourceWildcard);
return (
<Box className="mythic-api-token-resource-card p-6 flex flex-column gap-5 min-w-0 rounded bg-surface-raised border-subtle" key={resource}>
<Box className="mythic-api-token-resource-header items-start flex flex-wrap gap-6 justify-between min-w-0">
<Box sx={{minWidth: 0}}>
<Typography className="mythic-api-token-resource-title text-sm font-800 leading-120 text-primary">
{resource.split("_").join(" ")}
</Typography>
<Typography className="mythic-api-token-resource-subtitle">
{resourceScopes.length === 1 ? "1 available scope" : `${resourceScopes.length} available scopes`}
</Typography>
</Box>
<Box
className={`mythic-api-token-resource-wildcard text-xs font-750 items-center inline-flex flex-none gap-2 rounded cursor-pointer bg-neutral-1 border-subtle text-muted${resourceWildcardSelected ? " mythic-api-token-resource-wildcard-selected" : ""}${!canGrantResourceWildcard ? " mythic-api-token-resource-wildcard-disabled" : ""}`}
component="label"
>
<Checkbox
size="small"
disabled={scopeIsSelected("*") || !canGrantResourceWildcard}
checked={resourceWildcardSelected}
onChange={() => toggleScope(resourceWildcard)}
/>
<span>{resourceWildcard}</span>
</Box>
</Box>
<Box className="mythic-api-token-scope-grid gap-4 min-w-0 grid">
{resourceScopes.map(scope => {
const includedByWildcard = scopeIsSelected("*") || scopeIsSelected(resourceWildcard);
const scopeSelected = scopeIsSelected(scope.name) || includedByWildcard;
return (
<Box
className={`mythic-api-token-scope-card items-start flex gap-3 rounded cursor-pointer bg-surface border-subtle text-primary${scopeSelected ? " mythic-api-token-scope-card-selected mythic-tone-primary bg-tone-1 border-tone-3" : ""}${includedByWildcard ? " mythic-api-token-scope-card-inherited" : ""} min-w-0`}
component="label"
key={scope.name}
>
<Checkbox
size="small"
disabled={includedByWildcard}
checked={scopeSelected}
onChange={() => toggleScope(scope.name)}
/>
<Box className="mythic-api-token-scope-card-copy min-w-0 w-full">
<Box className="mythic-api-token-scope-card-title-row items-start flex gap-4 justify-between min-w-0">
<Typography className="mythic-api-token-scope-card-title text-sm font-800 leading-125 min-w-0 wrap-anywhere text-primary">{scope.display_name || scope.name}</Typography>
<MythicChip
compact
label={scope.access || "scope"}
size="small"
tone={scope.access === "read" ? "info" : ["write", "create", "update"].includes(scope.access) ? "success" : ["delete", "admin"].includes(scope.access) ? "error" : "neutral"}
/>
</Box>
<Typography className="mythic-api-token-scope-name text-xs leading-135 wrap-anywhere text-muted font-mono">
{scope.name}
</Typography>
<Typography className="mythic-api-token-scope-card-description text-xs leading-135 wrap-anywhere text-muted">
{scope.description}
</Typography>
{scope.includes?.length > 0 &&
<Typography className="mythic-api-token-scope-includes text-xs leading-135 font-700 wrap-anywhere mythic-tone-info text-tone">
Includes {scope.includes.join(", ")}
</Typography>
}
</Box>
</Box>
)
})}
</Box>
</Box>
);
})}
</>
}
</Box>
</Box>
)
}
@@ -0,0 +1,82 @@
import React from 'react';
import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import {normalizeMythicChipTone} from './MythicChip';
import {MythicStyledTooltip} from './MythicStyledTooltip';
export const MythicActionButton = React.forwardRef(function MythicActionButton({
active = false,
appearance, // can be plain or raised
autoWidth = false,
children,
className = "",
colorMode = "hover", // can be always or hover
compact = false,
icon,
iconOnly = false,
label,
muted = false,
rotated = false,
selected = false,
shape, // can be square or round
size = "small",
startIcon,
tone = "neutral",
tooltip,
tooltipStyle = {},
variant = "outlined",
...props
}, ref) {
const resolvedTone = normalizeMythicChipTone(tone);
const resolvedAppearance = appearance || (iconOnly ? "plain" : "raised");
const resolvedShape = shape || (resolvedAppearance === "raised" ? "square" : "round");
const resolvedColorMode = colorMode === "always" ? "always" : "hover";
const shapeClass = iconOnly ? "mythic-action-button-icon" : "mythic-action-button-label";
const resolvedClassName = [
"mythic-action-button",
`mythic-action-button-${resolvedAppearance}`,
shapeClass,
`mythic-action-button-shape-${resolvedShape}`,
`mythic-action-button-color-${resolvedColorMode}`,
`mythic-tone-${resolvedTone}`,
active || selected ? "mythic-action-button-active" : "",
autoWidth ? "mythic-action-button-auto-width" : "",
compact ? "mythic-action-button-compact" : "",
muted ? "mythic-action-button-muted" : "",
rotated ? "mythic-action-button-rotated" : "",
className,
].filter(Boolean).join(" ");
const ariaLabel = props["aria-label"] || tooltip || (typeof label === "string" ? label : undefined);
const content = label ?? children;
const button = iconOnly ? (
<IconButton
ref={ref}
aria-label={ariaLabel}
size={size}
{...props}
className={resolvedClassName}
color="inherit"
>
{icon || children}
</IconButton>
) : (
<Button
ref={ref}
aria-label={ariaLabel}
size={size}
startIcon={icon || startIcon}
variant={variant}
{...props}
className={resolvedClassName}
color="inherit"
>
{content}
</Button>
);
return tooltip ? (
<MythicStyledTooltip title={tooltip} tooltipStyle={{display: "inline-flex", ...tooltipStyle}}>
{button}
</MythicStyledTooltip>
) : button;
});
@@ -0,0 +1,55 @@
import {useTheme} from '@mui/material/styles';
import {MythicStyledTooltip} from "./MythicStyledTooltip";
import WifiIcon from '@mui/icons-material/Wifi';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import { faLink } from '@fortawesome/free-solid-svg-icons';
import {ImageWithAuth} from "../utilities/ImageWithAuth";
export const MythicAgentSVGIcon = ({payload_type, style, is_p2p}) => {
const theme = useTheme();
if(payload_type === ""){
return;
}
return (
<MythicStyledTooltip title={payload_type} >
<ImageWithAuth src={"/static/" + payload_type + "_" + theme.palette.mode + ".svg"}
style={{...style}} />
{is_p2p === false &&
<WifiIcon style={{width: "20px", height: "20px", marginLeft: "-20px", marginBottom: "5px",
borderRadius: "10px", border: "1px solid",
backgroundColor: theme.palette.background.default}}/>
}
{is_p2p === true &&
<FontAwesomeIcon icon={faLink} style={{width: "20px", height: "20px", marginLeft: "-20px", marginBottom: "5px",
borderRadius: "10px", border: "1px solid",
backgroundColor: theme.palette.background.default}} />
}
</MythicStyledTooltip>
)
}
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}>
<ImageWithAuth 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",
backgroundColor: theme.palette.primary.main, color: "white" }} />
}
{is_p2p === true &&
<FontAwesomeIcon icon={faLink} style={{width: "10px", height: "10px", marginLeft: "-5px", marginBottom: "0px",
borderRadius: "10px", border: "1px solid",
backgroundColor: theme.palette.primary.main, color: "white"}} />
}
</div>
)
}
@@ -0,0 +1,50 @@
import React from 'react';
import {useTheme} from '@mui/material/styles';
import SmartToyTwoToneIcon from '@mui/icons-material/SmartToyTwoTone';
import {ImageWithAuth} from "../utilities/ImageWithAuth";
const defaultIconProps = {fontSize: "small"};
const shallowEqualObjects = (left = {}, right = {}) => {
if(left === right){
return true;
}
const leftKeys = Object.keys(left);
const rightKeys = Object.keys(right);
if(leftKeys.length !== rightKeys.length){
return false;
}
return leftKeys.every((key) => left[key] === right[key]);
};
const MythicChatContainerIconComponent = ({containerName, className = "", imgClassName = "", iconProps = defaultIconProps, altText}) => {
const theme = useTheme();
const [imageFailed, setImageFailed] = React.useState(false);
const iconName = String(containerName || "").trim();
const imageSrc = iconName ? `/static/${iconName}_${theme.palette.mode}.svg` : "";
const handleImageError = React.useCallback(() => {
setImageFailed(true);
}, []);
React.useEffect(() => {
setImageFailed(false);
}, [imageSrc]);
if(!imageSrc || imageFailed){
return <SmartToyTwoToneIcon {...iconProps} />;
}
return (
<ImageWithAuth
alt={altText || iconName}
className={`${className} ${imgClassName}`.trim()}
onError={handleImageError}
src={imageSrc}
/>
);
};
export const MythicChatContainerIcon = React.memo(MythicChatContainerIconComponent, (previousProps, nextProps) => (
previousProps.containerName === nextProps.containerName &&
previousProps.className === nextProps.className &&
previousProps.imgClassName === nextProps.imgClassName &&
previousProps.altText === nextProps.altText &&
shallowEqualObjects(previousProps.iconProps || defaultIconProps, nextProps.iconProps || defaultIconProps)
));
@@ -0,0 +1,69 @@
import React from 'react';
import Chip from '@mui/material/Chip';
import {useTheme} from "@mui/material/styles";
const supportedTones = new Set([
"primary",
"secondary",
"neutral",
"info",
"success",
"warning",
"error",
]);
export const normalizeMythicChipTone = (tone) => supportedTones.has(tone) ? tone : "secondary";
const ChipPrimitive = React.forwardRef(function ChipPrimitive({
className = "",
color = undefined,
compact = false,
iconOnly = false,
muted = false,
shape,
size = "small",
tone = undefined,
variant = "outlined",
...props
}, ref) {
const resolvedTone = normalizeMythicChipTone(tone || (color ? color : "secondary"));
const muiVariant = variant ? variant : "outlined";
const hasCustomColor = Boolean(color) && !supportedTones.has(color);
const resolvedClassName = [
"mythic-chip-base",
"text-xs",
"font-800",
"items-center",
"inline-flex",
"gap-2",
"max-w-full",
"min-w-0",
"truncate",
"whitespace-nowrap",
shape === "square" ? "mythic-square-chip" : "mythic-chip",
hasCustomColor ? "mythic-chip-custom-color" : `mythic-tone-${resolvedTone}`,
compact ? "mythic-chip-compact text-2xs" : "",
muted ? "mythic-chip-muted" : "",
iconOnly ? "mythic-chip-icon-only justify-center" : "",
variant === "outlined" ? "mythic-chip-outlined" : "",
className,
].filter(Boolean).join(" ");
return (
<Chip
ref={ref}
className={resolvedClassName}
//color={color ? color : undefined}
size={size}
variant={muiVariant}
{...props}
/>
);
});
export const MythicChip = React.forwardRef(function MythicChip(props, ref) {
return <ChipPrimitive ref={ref} shape="pill" {...props} />;
});
export const SquareChip = React.forwardRef(function SquareChip(props, ref) {
return <ChipPrimitive ref={ref} shape="square" {...props} />;
});
@@ -0,0 +1,120 @@
import React from 'react';
import Box from '@mui/material/Box';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import Popper from '@mui/material/Popper';
import {HexColorInput, HexColorPicker} from 'react-colorful';
export const isValidHexColor = (color) => {
if(typeof color !== "string"){
return false;
}
if(color.length !== 7){
return false;
}
if(color[0] !== "#"){
return false;
}
return /^#[0-9a-fA-F]{6}$/.test(color);
}
export const getReadableTextColor = (backgroundColor) => {
if(!isValidHexColor(backgroundColor)){
return "#ffffff";
}
const red = parseInt(backgroundColor.slice(1, 3), 16);
const green = parseInt(backgroundColor.slice(3, 5), 16);
const blue = parseInt(backgroundColor.slice(5, 7), 16);
const brightness = (red * 299 + green * 587 + blue * 114) / 1000;
return brightness >= 140 ? "#111827" : "#ffffff";
}
export const MythicColorSwatchInput = ({color, label, onChange, inputWidth = "96px", sx = {}}) => {
const [pickerOpen, setPickerOpen] = React.useState(false);
const swatchRef = React.useRef(null);
const safeColor = isValidHexColor(color) ? color : "#000000";
return (
<ClickAwayListener onClickAway={() => setPickerOpen(false)}>
<Box sx={{display: "flex", alignItems: "center", gap: 1, minWidth: 0, position: "relative", ...sx}}>
<Box
component="button"
type="button"
ref={swatchRef}
onClick={(evt) => {
evt.preventDefault();
evt.stopPropagation();
setPickerOpen((open) => !open);
}}
aria-label={label}
aria-expanded={pickerOpen}
sx={{
alignItems: "center",
backgroundColor: "transparent",
border: "1px solid",
borderColor: "divider",
borderRadius: "4px",
cursor: "pointer",
display: "inline-flex",
flex: "0 0 auto",
height: 30,
justifyContent: "center",
p: "2px",
width: 34,
}}
>
<Box
sx={{
backgroundColor: safeColor,
borderRadius: "3px",
height: "100%",
width: "100%",
}}
/>
</Box>
<Popper
open={pickerOpen}
anchorEl={swatchRef.current}
placement="bottom-start"
style={{zIndex: 100000}}
>
<Box
onMouseDown={(evt) => evt.stopPropagation()}
onClick={(evt) => evt.stopPropagation()}
sx={{
backgroundColor: "background.paper",
border: "1px solid",
borderColor: "divider",
borderRadius: "6px",
boxShadow: 8,
mt: 0.5,
p: 1,
"& .react-colorful": {
width: 210,
height: 160,
},
}}
>
<HexColorPicker color={safeColor} onChange={onChange} />
</Box>
</Popper>
<HexColorInput
color={color}
onChange={onChange}
prefixed={true}
style={{
width: inputWidth,
minWidth: 0,
height: "30px",
boxSizing: "border-box",
borderRadius: "4px",
border: "1px solid rgba(128,128,128,0.45)",
background: "transparent",
color: "inherit",
padding: "0 8px",
fontFamily: "inherit",
fontSize: "0.85rem",
}}
/>
</Box>
</ClickAwayListener>
);
}
@@ -1,10 +1,9 @@
import React from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogTitle from '@mui/material/DialogTitle';
import {MythicDialog} from './MythicDialog';
import DialogContentText from '@mui/material/DialogContentText';
import DialogContent from '@mui/material/DialogContent';
import {MythicDialogButton, MythicDialogFooter} from "./MythicDialogLayout";
export function MythicConfirmDialog(props) {
const handleSubmit = () => {
@@ -14,6 +13,10 @@ export function MythicConfirmDialog(props) {
}
props.onClose();
}
const acceptIntent = props.acceptColor === "success" ? "primary" :
props.acceptColor === "warning" ? "warning" :
props.acceptColor === "info" ? "info" :
"destructive";
return (
<MythicDialog fullWidth={false} maxWidth="sm" open={props.open} onClose={()=>{props.onClose()}} innerDialog={
@@ -26,14 +29,14 @@ export function MythicConfirmDialog(props) {
</DialogContentText>
</DialogContent>
)}
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
{props.cancelText ? (props.cancelText) : ("Cancel")}
</Button>
<Button onClick={handleSubmit} autoFocus variant="contained" color={props.acceptColor ? (props.acceptColor) : ("error")}>
</MythicDialogButton>
<MythicDialogButton onClick={handleSubmit} autoFocus intent={acceptIntent}>
{props.acceptText ? (props.acceptText) : ("Remove")}
</Button>
</DialogActions>
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
} />
);
@@ -1,10 +1,8 @@
import {MythicActionButton} from "./MythicActionButton";
import React, {useEffect} from 'react';
import Dialog from '@mui/material/Dialog';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import MythicTextField from './MythicTextField';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
@@ -13,11 +11,91 @@ import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import { toLocalTime } from '../utilities/Time';
import AceEditor from 'react-ace';
import "ace-builds/src-noconflict/mode-json";
import "ace-builds/src-noconflict/theme-github";
import "ace-builds/src-noconflict/theme-monokai";
import "ace-builds/src-noconflict/ext-searchbox";
import {useTheme} from '@mui/material/styles';
import WrapTextIcon from '@mui/icons-material/WrapText';
import {MythicStyledTooltip} from "./MythicStyledTooltip";
import Draggable from 'react-draggable';
import {MythicDraggableDialogTitle} from "./MythicDraggableDialogTitle";
import {MythicDialogButton, MythicDialogFooter} from "./MythicDialogLayout";
import {MythicChip} from "./MythicChip";
let mythicDialogIdCounter = 0;
let mythicDialogStack = [];
export function MythicDialog(props) {
const descriptionElementRef = React.useRef(null);
React.useEffect(() => {
const MythicDraggableDialogPaper = React.forwardRef(function MythicDraggableDialogPaper(props, ref) {
const {
draggableCancel,
draggableHandle,
draggableOnStart,
draggableOnStop,
...paperProps
} = props;
const nodeRef = React.useRef(null);
const setPaperRef = React.useCallback((node) => {
nodeRef.current = node;
if(typeof ref === "function"){
ref(node);
} else if(ref){
ref.current = node;
}
}, [ref]);
const onStart = React.useCallback((event, data) => {
return draggableOnStart?.(event, data, nodeRef.current);
}, [draggableOnStart]);
return (
<Draggable
nodeRef={nodeRef}
handle={draggableHandle}
cancel={draggableCancel}
onStart={onStart}
onStop={draggableOnStop}
>
<Paper {...paperProps} ref={setPaperRef} />
</Draggable>
);
});
// Kept temporarily for route-level compatibility while consumers migrate to the lightweight base module.
// eslint-disable-next-line no-unused-vars
function LegacyMythicDialog(props) {
const theme = useTheme();
const dialogIdRef = React.useRef(null);
if(dialogIdRef.current === null){
dialogIdRef.current = mythicDialogIdCounter++;
}
const [draggedState, setDraggedState] = React.useState({
style: {},
paperStyle: {
margin: "0",
height: "fit-content",
width: "stretch"
},
containerStyle: {
},
hideBackdrop: false,
modified: false
});
const descriptionElementRef = React.useRef(null);
React.useEffect(() => {
if(!props.open){
return undefined;
}
mythicDialogStack = mythicDialogStack.filter((id) => id !== dialogIdRef.current);
mythicDialogStack.push(dialogIdRef.current);
return () => {
mythicDialogStack = mythicDialogStack.filter((id) => id !== dialogIdRef.current);
};
}, [props.open]);
const isTopDialog = () => {
return mythicDialogStack[mythicDialogStack.length - 1] === dialogIdRef.current;
};
React.useEffect(() => {
if (props.open) {
const { current: descriptionElement } = descriptionElementRef;
if (descriptionElement !== null) {
@@ -26,198 +104,389 @@ export function MythicDialog(props) {
}
}, [props.open]);
const dialogOnClick = (e) => {
e.stopPropagation();
if(e.target.classList.length > 0 && e.target.classList.contains("MuiDialog-container")){
props.onClose();
if(e.target.classList?.contains("MuiDialog-container")){
e.stopPropagation();
if(!isTopDialog()){
return;
}
if(draggedState.hideBackdrop){
props.onClose?.();
}
}
}
const dialogOnContextMenu = (e) => {
e.stopPropagation();
}
const handleOnClose = (event, reason) => {
if(reason === "backdropClick" || reason === "escapeKeyDown"){
event?.stopPropagation?.();
if(!isTopDialog()){
return;
}
}
if(reason === "backdropClick" && draggedState.hideBackdrop){
return;
}
props.onClose?.();
}
const onStart = React.useCallback((e, _data, paperNode) => {
if(e){
e.stopPropagation();
e.preventDefault();
}
setDraggedState((currentState) => {
if(currentState.modified){
return currentState;
}
const dragTarget = paperNode || e?.target?.closest?.(".MuiPaper-root") || e?.target?.offsetParent;
const targetWidth = dragTarget?.offsetWidth || e?.target?.offsetParent?.offsetWidth || 0;
return {
style: {},
paperStyle: {
...(targetWidth > 0 ? {width: targetWidth + "px"} : {}),
margin: 0,
overflowY: "auto",
},
containerStyle: {
overflow: "visible",
},
hideBackdrop: true,
modified: true,
};
});
}, []);
const onStop = React.useCallback((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"
onClick={dialogOnClick}
>
{props.innerDialog}
</Dialog>
<Dialog
open={props.open}
onClose={handleOnClose}
scroll="paper"
maxWidth={props.maxWidth}
fullWidth={true}
PaperComponent={MythicDraggableDialogPaper}
PaperProps={{
draggableHandle: "#mythic-draggable-title",
draggableCancel: '[class*="MuiDialogContent-root"]',
draggableOnStart: onStart,
draggableOnStop: onStop,
}}
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": {
borderRadius: "8px",
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.borderColor}`,
...draggedState.paperStyle
},
".MuiDialog-container": {
...draggedState.containerStyle
}
}}
onMouseDown={dialogOnClick}
onContextMenu={dialogOnContextMenu}
>
{props.innerDialog}
</Dialog>
);
}
export {MythicDialog} from "./MythicDialogBase";
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);
if(props.dontCloseOnSubmit){
return;
}
props.onClose();
}
const onChange = (name, value, error) => {
const onChange = (value) => {
setComment(value);
}
useEffect( () => {
setComment(props.value);
try{
setComment(JSON.stringify(JSON.parse(props.value), null, 2));
}catch(error){
setComment(props.value);
}
}, [props.value]);
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">{props.title}</DialogTitle>
<DialogContent dividers={true}>
<MythicTextField autoFocus onEnter={props?.onEnter || onCommitSubmit} onChange={onChange} value={comment} multiline={props?.multiline || false} maxRows={props.maxRows} />
{props.title !== "" &&
<MythicDraggableDialogTitle>{props.title}
<MythicStyledTooltip title={wrap ? "Toggle off word wrap" : "Toggl on word wrap"}
tooltipStyle={{float: "right"}}>
<MythicActionButton iconOnly onClick={() => {setWrap(!wrap)}}>
<WrapTextIcon color={wrap ? "success" : "secondary"} />
</MythicActionButton>
</MythicStyledTooltip>
</MythicDraggableDialogTitle>
}
<DialogContent dividers={true} style={{ margin: 0, padding: 0}}>
<AceEditor
mode="json"
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
width="100%"
fontSize={14}
showPrintMargin={false}
wrapEnabled={wrap}
value={comment}
focus={true}
onChange={onChange}
setOptions={{
tabSize: 4,
useWorker: false,
showInvisibles: false,
}}
/>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
Close
</Button>
<Button onClick={onCommitSubmit} variant="contained" color="success">
Submit
</Button>
</DialogActions>
{ (props.onClose || props.onSubmit) &&
<MythicDialogFooter>
{props.onClose &&
<MythicDialogButton onClick={props.onClose}>
Close
</MythicDialogButton>
}
{props.onSubmit &&
<MythicDialogButton intent="primary" onClick={onCommitSubmit}>
{props.onSubmitText ? props.onSubmitText : "Submit"}
</MythicDialogButton>
}
</MythicDialogFooter>
}
</React.Fragment>
);
}
export function MythicViewJSONAsTableDialog(props) {
const [comment, setComment] = React.useState([]);
const [tableType, setTableType] = React.useState("dictionary");
const [headers, setHeaders] = React.useState([]);
useEffect( () => {
let permissions = [];
try{
let permissionDict;
if(props.value.constructor === Object){
permissionDict = props.value;
}else{
permissionDict = JSON.parse(props.value);
}
if(permissionDict.constructor === Object){
for(let key in permissionDict){
if(permissionDict[key] && permissionDict[key].constructor === Object){
// potentially have a nested dictionary here or array to become a dictionary, mark it
permissions.push({"name": key, "value": permissionDict[key], new_table: true, is_dictionary: true, headers: ["Name", "Value"]});
} else if(permissionDict[key] && Array.isArray(permissionDict[key])) {
if (permissionDict[key].length === 1){
if(permissionDict[key][0].constructor === Object){
permissions.push({"name": key, "value": permissionDict[key][0], new_table: true, is_dictionary: true, headers: ["Name", "Value"]});
}else{
permissions.push({"name": key, "value": JSON.stringify(permissionDict[key], null, 2)});
}
} else if (permissionDict[key].length > 1) {
if (permissionDict[key][0].constructor === Object) {
let newHeaders = [];
for(let i = 0; i < permissionDict[key].length; i++){
for(let newKey in permissionDict[key][i]){
if(!newHeaders.includes(newKey)){newHeaders.push(newKey)}
}
}
newHeaders.sort()
permissions.push({"name": key, "value": permissionDict[key], new_table: true, is_array: true, headers: newHeaders});
} else {
// it's an array, but not of dictionaries, so just stringify it
permissions.push({"name": key, "value": JSON.stringify(permissionDict[key], null, 2)});
}
} else {
permissions.push({"name": key, "value": JSON.stringify(permissionDict[key], null, 2)});
}
}else if(permissionDict[key] !== undefined && permissionDict[key] !== null){
permissions.push({"name": key, "value": permissionDict[key]});
}
setHeaders([props.leftColumn, props.rightColumn]);
}
}else{
setTableType("array");
if(permissionDict.length > 0){
setHeaders(Object.keys(permissionDict[0]));
permissions = permissionDict;
}else{
setHeaders([]);
}
}
}catch(error){
console.log(error);
const isPlainObject = (value) => {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
const parseJSONDialogValue = (value) => {
if(value === undefined || value === null){
return value;
}
if(typeof value !== "string"){
return value;
}
try{
return JSON.parse(value);
}catch(error){
return value;
}
}
const getJSONValueType = (value) => {
if(value === null){return "null"}
if(value === undefined){return "empty"}
if(Array.isArray(value)){return "array"}
if(isPlainObject(value)){return "object"}
return typeof value;
}
const getJSONValueCount = (value) => {
if(Array.isArray(value)){return value.length}
if(isPlainObject(value)){return Object.keys(value).length}
if(value === undefined || value === null || value === ""){return 0}
return 1;
}
const getArrayObjectHeaders = (rows) => {
const headers = [];
rows.forEach((row) => {
if(!isPlainObject(row)){return}
Object.keys(row).forEach((key) => {
if(!headers.includes(key)){
headers.push(key);
}
setComment(permissions);
}, [props.value, props.leftColumn, props.rightColumn]);
});
});
return headers;
}
const JSONTypeBadge = ({value}) => {
const type = getJSONValueType(value);
const count = getJSONValueCount(value);
const label = type === "array" ? `${count} item${count === 1 ? "" : "s"}` :
type === "object" ? `${count} field${count === 1 ? "" : "s"}` :
type;
return <MythicChip compact label={label} tone={["array", "object"].includes(type) ? "info" : "neutral"} />;
}
const JSONPrimitiveValue = ({name, value, me}) => {
const type = getJSONValueType(value);
if(type === "null" || type === "empty" || value === ""){
return <span className="mythic-json-value-empty text-xs text-disabled">None</span>
}
if(type === "boolean"){
return <MythicChip compact label={value ? "True" : "False"} tone={value ? "success" : "warning"} />;
}
return <span className="mythic-json-value-primitive text-primary text-xs leading-135 wrap-anywhere">{convertValueToContextValue(name, value, me)}</span>
}
const JSONTableCellValue = ({name, value, me, depth}) => {
if(Array.isArray(value) || isPlainObject(value)){
return (
<JSONTableValue
label={name}
value={value}
me={me}
depth={depth + 1}
/>
);
}
return <JSONPrimitiveValue name={name} value={value} me={me} />;
}
const JSONTableValue = ({label, value, me, depth=0, leftColumn="Name", rightColumn="Value"}) => {
const type = getJSONValueType(value);
const showPanelHeader = Boolean(label) && depth === 0;
if(type !== "array" && type !== "object"){
return <JSONPrimitiveValue name={label || ""} value={value} me={me} />;
}
if(type === "object"){
const entries = Object.entries(value);
return (
<div className={`mythic-json-panel p-4 flex flex-column gap-4 rounded bg-neutral-1 border-subtle${depth === 0 ? " mythic-json-panel-root bg-surface-muted" : ""} min-w-0`}>
{showPanelHeader &&
<div className="mythic-json-panel-header items-center flex flex-wrap gap-4 justify-between min-w-0">
<span className="mythic-json-panel-title text-xs font-800 leading-125 min-w-0 wrap-anywhere text-primary">{label}</span>
<JSONTypeBadge value={value} />
</div>
}
{entries.length === 0 ? (
<div className="mythic-json-empty-state text-xs text-muted">No fields to display.</div>
) : (
<TableContainer className="mythic-json-table-wrap overflow-auto rounded border-subtle">
<Table size="small" stickyHeader={depth === 0} style={{tableLayout: "fixed"}}>
<TableHead>
<TableRow>
<TableCell style={{width: depth === 0 ? "14rem" : "12rem"}}>{leftColumn}</TableCell>
<TableCell>{rightColumn}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{entries.map(([key, entryValue]) => (
<TableRow key={`${depth}-${key}`} hover>
<TableCell className="mythic-json-key-cell">
<div className="mythic-json-key-stack flex flex-column gap-2 min-w-0">
<span className="mythic-json-key text-xs font-800 leading-125 wrap-anywhere text-primary">{key}</span>
<JSONTypeBadge value={entryValue} />
</div>
</TableCell>
<TableCell className="mythic-json-value-cell text-primary text-xs leading-135 wrap-anywhere">
<JSONTableCellValue name={key} value={entryValue} me={me} depth={depth} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</div>
);
}
const objectHeaders = getArrayObjectHeaders(value);
const isObjectArray = value.length > 0 && objectHeaders.length > 0 && value.every((row) => isPlainObject(row));
return (
<div className={`mythic-json-panel p-4 flex flex-column gap-4 rounded bg-neutral-1 border-subtle${depth === 0 ? " mythic-json-panel-root bg-surface-muted" : ""} min-w-0`}>
{showPanelHeader &&
<div className="mythic-json-panel-header items-center flex flex-wrap gap-4 justify-between min-w-0">
<span className="mythic-json-panel-title text-xs font-800 leading-125 min-w-0 wrap-anywhere text-primary">{label}</span>
<JSONTypeBadge value={value} />
</div>
}
{value.length === 0 ? (
<div className="mythic-json-empty-state text-xs text-muted">No items to display.</div>
) : isObjectArray ? (
<TableContainer className="mythic-json-table-wrap overflow-auto rounded border-subtle">
<Table size="small" stickyHeader={depth === 0} style={{tableLayout: "fixed", minWidth: `${Math.max(38, objectHeaders.length * 12)}rem`}}>
<TableHead>
<TableRow>
<TableCell style={{width: "4rem"}}>#</TableCell>
{objectHeaders.map((header) => (
<TableCell key={`array-header-${header}`} style={{width: "12rem"}}>{header}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{value.map((row, rowIndex) => (
<TableRow key={`array-row-${rowIndex}`} hover>
<TableCell className="mythic-json-index-cell text-xs font-800 text-muted text-right">{rowIndex + 1}</TableCell>
{objectHeaders.map((header) => (
<TableCell key={`array-row-${rowIndex}-${header}`} className="mythic-json-value-cell text-primary text-xs leading-135 wrap-anywhere">
<JSONTableCellValue name={header} value={row[header]} me={me} depth={depth} />
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
) : (
<TableContainer className="mythic-json-table-wrap overflow-auto rounded border-subtle">
<Table size="small" stickyHeader={depth === 0} style={{tableLayout: "fixed"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "4rem"}}>#</TableCell>
<TableCell>Value</TableCell>
</TableRow>
</TableHead>
<TableBody>
{value.map((entryValue, rowIndex) => (
<TableRow key={`array-value-row-${rowIndex}`} hover>
<TableCell className="mythic-json-index-cell text-xs font-800 text-muted text-right">{rowIndex + 1}</TableCell>
<TableCell className="mythic-json-value-cell text-primary text-xs leading-135 wrap-anywhere">
<JSONTableCellValue name={`${label || "value"} ${rowIndex + 1}`} value={entryValue} me={me} depth={depth} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</div>
);
}
export function MythicViewJSONAsTableDialog(props) {
const parsedValue = React.useMemo(() => parseJSONDialogValue(props.value), [props.value]);
const rootLabel = props.title || "JSON Data";
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">{props.title}</DialogTitle>
<Paper elevation={5} style={{position: "relative"}} variant={"elevation"}>
<TableContainer className="mythicElement">
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
<TableHead>
<TableRow>
{headers.map( (header, index) => (
<TableCell key={'header' + index} style={index === 0 ? {width: "30%"} : {}}>{header}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{tableType === "dictionary" ? (
comment.map( (element, index) => (
<TableRow key={'row' + index} hover>
<TableCell>{element.name}</TableCell>
{element.new_table ?
(
<TableContainer className="mythicElement">
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
<TableHead>
<TableRow>
{element.headers.map( (header, index) => (
<TableCell key={'eheader' + header + index} style={index === 0 ? {width: "30%"} : {}}>{header}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{element.is_dictionary ? (
Object.keys(element.value).map( (key, dictIndex) => (
<TableRow key={'element' + dictIndex + "dictheader"}>
<TableCell style={{width: "30%", wordBreak: "break-all"}}>{key}</TableCell>
<TableCell style={{wordBreak: "break-all"}}>{convertValueToContextValue(key, element.value[key], props.me)}</TableCell>
</TableRow>
))
): (
element.value.map( (e, elementIndex) => (
<TableRow>
{element.headers.map( (header, headerIndex) => (
<TableCell key={'element' + elementIndex + "header" + headerIndex} style={headerIndex === 0 ? {width: "30%"} : {}}>{convertValueToContextValue(header, e[header], props.me)}</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
)
:
(<TableCell style={{wordBreak: "break-all", whiteSpace: "pre-wrap"}}>{convertValueToContextValue(element.name, element.value, props.me) }</TableCell>)
}
</TableRow>
))
) : (
comment.map( (row, index) => (
<TableRow key={'row' + index} hover>
{Object.keys(row).map( (key) => (
<TableCell key={"row" + index + "cell" + key}>{convertValueToContextValue(key, row[key], props.me)}</TableCell>
))}
</TableRow>
))
) }
</TableBody>
</Table>
</TableContainer>
</Paper>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
<MythicDraggableDialogTitle style={{wordBreak: "break-all", maxWidth: "100%"}}>
<div className="mythic-json-title-row items-center flex flex-wrap gap-4 justify-between min-w-0 w-full">
<span>{rootLabel}</span>
<JSONTypeBadge value={parsedValue} />
</div>
</MythicDraggableDialogTitle>
<DialogContent dividers={true} className="mythic-dialog-body flex flex-column gap-6 mythic-json-dialog-body min-w-0 w-full overflow-auto">
<JSONTableValue
value={parsedValue}
me={props.me}
leftColumn={props.leftColumn || "Name"}
rightColumn={props.rightColumn || "Value"}
/>
</DialogContent>
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
</DialogActions>
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
);
}
@@ -241,7 +510,7 @@ export function MythicViewObjectPropertiesAsTableDialog(props) {
<DialogTitle id="form-dialog-title">{props.title}</DialogTitle>
<DialogContent dividers={true}>
<Paper elevation={5} style={{position: "relative"}} variant={"elevation"}>
<TableContainer component={Paper} className="mythicElement">
<TableContainer component={Paper}>
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
<TableHead>
<TableRow>
@@ -252,8 +521,8 @@ export function MythicViewObjectPropertiesAsTableDialog(props) {
<TableBody>
{comment.map( (element, index) => (
<TableRow key={'row' + index}>
<TableCell>{element.name}</TableCell>
<TableCell>{convertValueToContextValue(element.name, element.value)}</TableCell>
<TableCell style={{wordBreak: "break-all"}}>{element.name}</TableCell>
<TableCell style={{wordBreak: "break-all"}}>{convertValueToContextValue(element.name, element.value)}</TableCell>
</TableRow>
))}
</TableBody>
@@ -261,30 +530,34 @@ export function MythicViewObjectPropertiesAsTableDialog(props) {
</TableContainer>
</Paper>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
</DialogActions>
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
);
}
const convertValueToContextValue = (key, value, me) => {
if( key.includes("time") ){
const keyText = String(key || "").toLowerCase();
if(value === undefined || value === null){
return "";
}
if( keyText.includes("time") ){
try{
return TableRowDateCell({cellData: value, view_utc_time: me?.user?.view_utc_time})
}catch(error){
console.log("failed to parse metadata as date", key, value);
return value;
}
} else if( key.includes("size") ){
} else if( keyText.includes("size") ){
try{
return TableRowSizeCell({cellData: value})
}catch(error){
console.log("failed to parse metadata as size", key, value);
return value;
}
} else if (value.constructor === Object) {
} else if (isPlainObject(value)) {
return JSON.stringify(value, null, 2);
} else if (Array.isArray(value)){
return JSON.stringify(value, null, 2);
@@ -352,4 +625,4 @@ export const TableRowSizeCell = ({ cellData, rowData }) => {
}
};
return getStringSize(cellData);
};
};
@@ -0,0 +1,111 @@
import React from "react";
import Dialog from "@mui/material/Dialog";
import Paper from "@mui/material/Paper";
import Draggable from "react-draggable";
import {useTheme} from "@mui/material/styles";
let mythicDialogIdCounter = 0;
let mythicDialogStack = [];
const MythicDraggableDialogPaper = React.forwardRef(function MythicDraggableDialogPaper(props, ref) {
const {draggableCancel, draggableHandle, draggableOnStart, draggableOnStop, ...paperProps} = props;
const nodeRef = React.useRef(null);
const setPaperRef = React.useCallback((node) => {
nodeRef.current = node;
if(typeof ref === "function"){
ref(node);
}else if(ref){
ref.current = node;
}
}, [ref]);
const onStart = React.useCallback((event, data) => {
return draggableOnStart?.(event, data, nodeRef.current);
}, [draggableOnStart]);
return (
<Draggable nodeRef={nodeRef} handle={draggableHandle} cancel={draggableCancel}
onStart={onStart} onStop={draggableOnStop}>
<Paper {...paperProps} ref={setPaperRef} />
</Draggable>
);
});
export function MythicDialog(props) {
const theme = useTheme();
const dialogIdRef = React.useRef(null);
if(dialogIdRef.current === null){
dialogIdRef.current = mythicDialogIdCounter++;
}
const [draggedState, setDraggedState] = React.useState({
style: {},
paperStyle: {margin: "0", height: "fit-content", width: "stretch"},
containerStyle: {},
hideBackdrop: false,
modified: false,
});
const descriptionElementRef = React.useRef(null);
React.useEffect(() => {
if(!props.open){return undefined;}
mythicDialogStack = mythicDialogStack.filter((id) => id !== dialogIdRef.current);
mythicDialogStack.push(dialogIdRef.current);
return () => {
mythicDialogStack = mythicDialogStack.filter((id) => id !== dialogIdRef.current);
};
}, [props.open]);
const isTopDialog = () => mythicDialogStack[mythicDialogStack.length - 1] === dialogIdRef.current;
React.useEffect(() => {
if(props.open && descriptionElementRef.current !== null){
descriptionElementRef.current.focus();
}
}, [props.open]);
const dialogOnClick = (event) => {
if(event.target.classList?.contains("MuiDialog-container")){
event.stopPropagation();
if(isTopDialog() && draggedState.hideBackdrop){
props.onClose?.();
}
}
};
const handleOnClose = (event, reason) => {
if(reason === "backdropClick" || reason === "escapeKeyDown"){
event?.stopPropagation?.();
if(!isTopDialog()){return;}
}
if(reason === "backdropClick" && draggedState.hideBackdrop){return;}
props.onClose?.();
};
const onStart = React.useCallback((event, _data, paperNode) => {
event?.stopPropagation?.();
event?.preventDefault?.();
setDraggedState((currentState) => {
if(currentState.modified){return currentState;}
const dragTarget = paperNode || event?.target?.closest?.(".MuiPaper-root") || event?.target?.offsetParent;
const targetWidth = dragTarget?.offsetWidth || event?.target?.offsetParent?.offsetWidth || 0;
return {
style: {},
paperStyle: {...(targetWidth > 0 ? {width: `${targetWidth}px`} : {}), margin: 0, overflowY: "auto"},
containerStyle: {overflow: "visible"},
hideBackdrop: true,
modified: true,
};
});
}, []);
const onStop = React.useCallback((event) => {
event?.stopPropagation?.();
event?.preventDefault?.();
}, []);
return (
<Dialog open={props.open} onClose={handleOnClose} scroll="paper" maxWidth={props.maxWidth} fullWidth={true}
PaperComponent={MythicDraggableDialogPaper}
PaperProps={{draggableHandle: "#mythic-draggable-title", draggableCancel: '[class*="MuiDialogContent-root"]', draggableOnStart: onStart, draggableOnStop: onStop}}
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": {borderRadius: "8px", backgroundColor: theme.palette.background.paper, border: `1px solid ${theme.borderColor}`, ...draggedState.paperStyle},
".MuiDialog-container": {...draggedState.containerStyle},
}}
onMouseDown={dialogOnClick} onContextMenu={(event) => event.stopPropagation()}>
{props.innerDialog}
</Dialog>
);
}
@@ -0,0 +1,171 @@
import React from 'react';
import Box from '@mui/material/Box';
import {MythicActionButton} from './MythicActionButton';
import DialogActions from '@mui/material/DialogActions';
import Typography from '@mui/material/Typography';
const joinClasses = (...classes) => classes.filter(Boolean).join(" ");
export function MythicDialogBody({children, className, compact = false, ...props}) {
return (
<Box
className={joinClasses("mythic-dialog-body flex flex-column gap-6 min-w-0 w-full", compact && "mythic-dialog-body-compact gap-4", className)}
{...props}
>
{children}
</Box>
);
}
export function MythicDialogSection({title, description, actions, children, className, ...props}) {
return (
<Box component="section" className={joinClasses("mythic-dialog-section bg-surface-muted border-subtle min-w-0 rounded", className)} {...props}>
{(title || description || actions) &&
<Box className="mythic-dialog-section-header items-start flex gap-6 justify-between min-w-0">
<Box sx={{minWidth: 0}}>
{title &&
<Typography component="h3" className="mythic-dialog-section-title text-sm font-700 leading-125 text-primary">
{title}
</Typography>
}
{description &&
<Typography component="div" className="mythic-dialog-section-description">
{description}
</Typography>
}
</Box>
{actions &&
<Box className="mythic-dialog-section-actions items-center flex flex-none gap-3">
{actions}
</Box>
}
</Box>
}
{children}
</Box>
);
}
export function MythicDialogGrid({children, className, minWidth = "16rem", sx = {}, ...props}) {
return (
<Box
className={joinClasses("mythic-dialog-grid items-logical-start gap-5 min-w-0 w-full grid", className)}
sx={{"--mythic-dialog-grid-min": minWidth, ...sx}}
{...props}
>
{children}
</Box>
);
}
export function MythicDialogChoiceRow({children, className, ...props}) {
return (
<Box className={joinClasses("mythic-dialog-choice-row items-center gap-4 w-full grid", className)} {...props}>
{children}
</Box>
);
}
export function MythicDialogChoiceDivider({children = "OR", className, ...props}) {
return (
<Box component="span" className={joinClasses("mythic-dialog-choice-divider text-xs font-700 text-muted text-center", className)} {...props}>
{children}
</Box>
);
}
export function MythicDialogFooter({children, className, ...props}) {
return (
<DialogActions className={className} {...props}>
{children}
</DialogActions>
);
}
export function MythicDialogButton({children, className, intent = "secondary", ...props}) {
const tone = intent === "destructive" ? "error" : intent;
return (
<MythicActionButton
className={joinClasses("mythic-dialog-button", className)}
colorMode="always"
size="small"
tone={tone}
variant="contained"
{...props}
>
{children}
</MythicActionButton>
);
}
export function MythicForm({children, className, ...props}) {
return (
<Box component="form" className={joinClasses("mythic-form flex flex-column gap-6 min-w-0 w-full", className)} {...props}>
{children}
</Box>
);
}
export function MythicFormGrid({children, className, minWidth = "16rem", sx = {}, ...props}) {
return (
<Box
className={joinClasses("mythic-form-grid items-logical-start gap-6 min-w-0 w-full grid", className)}
sx={{"--mythic-form-grid-min": minWidth, ...sx}}
{...props}
>
{children}
</Box>
);
}
export function MythicFormField({children, className, description, label, required = false, ...props}) {
return (
<Box className={joinClasses("mythic-form-field flex flex-column gap-3 min-w-0", className)} {...props}>
{(label || description) &&
<Box className="mythic-form-field-copy min-w-0">
{label &&
<Typography component="label" className="mythic-form-field-label text-xs font-750 leading-125 text-primary">
{label}{required && <Box component="span" className="mythic-form-field-required"> *</Box>}
</Typography>
}
{description &&
<Typography component="div" className="mythic-form-field-description">
{description}
</Typography>
}
</Box>
}
<Box className="mythic-form-field-control min-w-0 w-full">
{children}
</Box>
</Box>
);
}
export function MythicFormNote({children, className, ...props}) {
return (
<Box className={joinClasses("mythic-form-note text-xs leading-140 rounded bg-neutral-1 border-subtle text-muted", className)} {...props}>
{children}
</Box>
);
}
export function MythicFormSwitchRow({control, label, description, className, ...props}) {
return (
<Box className={joinClasses("mythic-form-switch-row py-5 px-6 items-center flex gap-6 justify-between min-w-0 w-full rounded bg-surface border-subtle", className)} {...props}>
<Box sx={{minWidth: 0}}>
<Typography component="div" className="mythic-form-field-label text-xs font-750 leading-125 text-primary">
{label}
</Typography>
{description &&
<Typography component="div" className="mythic-form-field-description">
{description}
</Typography>
}
</Box>
<Box className="mythic-form-switch-control flex-none">
{control}
</Box>
</Box>
);
}
@@ -1,25 +1,16 @@
import React from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import {MythicDialog} from './MythicDialog';
import MythicTextField from './MythicTextField';
import {MythicDialog, MythicModifyStringDialog} from './MythicDialog';
export function MythicDisplayTextDialog(props) {
return (
<MythicDialog fullWidth={props.fullWidth === null ? false : props.fullWidth} maxWidth={props.maxWidth === null ? "sm" : props.maxWidth} open={props.open} onClose={()=>{props.onClose()}} innerDialog={
<React.Fragment>
<DialogTitle id="form-dialog-title">{props.title}</DialogTitle>
<DialogContent dividers={true}>
<MythicTextField multiline={true} value={props.value} onChange={()=>{}} />
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
Close
</Button>
</DialogActions>
</React.Fragment>
<MythicDialog fullWidth={props.fullWidth === null ? false : props.fullWidth}
maxWidth={props.maxWidth === null ? "sm" : props.maxWidth}
open={props.open}
onClose={()=>{props.onClose()}} innerDialog={
<MythicModifyStringDialog title={props.title}
onClose={props.onClose}
value={props.value}
maxRows={20} />
} />
);
}
@@ -0,0 +1,17 @@
import DialogTitle from '@mui/material/DialogTitle';
import {useTheme} from '@mui/material/styles';
export const MythicDraggableDialogTitle = ({children}) => {
const theme = useTheme();
return (
<DialogTitle id="mythic-draggable-title" style={{
cursor: 'move',
width: "100%",
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
}}>
{children}
</DialogTitle>
)
}
@@ -0,0 +1,20 @@
import {createPortal} from 'react-dom';
export const reorder = (
list,
startIndex,
endIndex
) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
export const MythicDraggablePortal = ({children, isDragging}) => {
if(isDragging && typeof document !== "undefined"){
return createPortal(children, document.body);
}
return children;
};
@@ -1,19 +1,23 @@
import React from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import TableCell from '@mui/material/TableCell';
import TableRow from '@mui/material/TableRow';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableContainer from '@mui/material/TableContainer';
import Paper from '@mui/material/Paper';
import { Divider, Input, MenuItem, Select } from '@mui/material';
import MythicTextField from './MythicTextField';
import FormControl from '@mui/material/FormControl';
import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select';
import TextField from '@mui/material/TextField';
import {gql } from '@apollo/client';
import {snackActions} from '../utilities/Snackbar';
import {useMutation } from '@apollo/client';
import {
MythicDialogBody,
MythicDialogButton,
MythicDialogFooter,
MythicDialogGrid,
MythicDialogSection,
MythicForm,
MythicFormField,
MythicFormNote
} from './MythicDialogLayout';
const submitFeedbackMutation = gql`
mutation submitFeedback($webhookType: String!, $webhookData: jsonb!){
@@ -25,15 +29,15 @@ const submitFeedbackMutation = gql`
`;
export function MythicFeedbackDialog(props) {
const [message, setMessage] = React.useState("");
const [taskID, setTaskID] = React.useState(0);
const [taskID, setTaskID] = React.useState("");
const messageTypeOptions = [
{display: "Bug", value: "bug"},
{display: "Feature Request", value: "feature_request"},
{display: "Confusing UI", value: "confusing_ui"},
{display: "Detection", value: "detection"},
{display: "Bug", value: "bug", description: "Something is broken or behaving unexpectedly."},
{display: "Feature Request", value: "feature_request", description: "A workflow, capability, or quality-of-life improvement."},
{display: "Confusing UI", value: "confusing_ui", description: "A part of the interface is unclear or hard to use."},
{display: "Detection", value: "detection", description: "Detection, telemetry, or visibility feedback."},
];
const [messageType, setMessageType] = React.useState("bug");
const [submitFeedback] = useMutation(submitFeedbackMutation, {
const [submitFeedback, {loading}] = useMutation(submitFeedbackMutation, {
update: (cache, {data}) => {
if(data.sendExternalWebhook.status === "success"){
snackActions.success("Submitted Feedback!");
@@ -48,90 +52,111 @@ export function MythicFeedbackDialog(props) {
}
});
const handleSubmit = () => {
let webhookData = {};
if(taskID > 0){
webhookData["task_id"] = String(taskID);
const trimmedMessage = message.trim();
if(trimmedMessage.length === 0){
snackActions.warning("Please include feedback before submitting.");
return;
}
webhookData["message"] = message;
let webhookData = {};
if(Number(taskID) > 0){
webhookData["task_display_id"] = String(Number(taskID));
}
webhookData["message"] = trimmedMessage;
webhookData["feedback_type"] = messageType;
submitFeedback({variables: {webhookType: "new_feedback", webhookData: webhookData}});
}
const handleMessageTypeChange = (evt) => {
setMessageType(evt.target.value);
}
const handleTaskIDChange = (name, value, error) => {
setTaskID(value);
const handleTaskIDChange = (evt) => {
setTaskID(evt.target.value);
}
const handleMessageChange = (name, value, error) => {
setMessage(value);
const handleMessageChange = (evt) => {
setMessage(evt.target.value);
}
const handleOnEnter = (event) => {
if( event.shiftKey){
handleSubmit();
}
const onSubmitForm = (event) => {
event.preventDefault();
handleSubmit();
}
const selectedMessageType = messageTypeOptions.find((opt) => opt.value === messageType);
const canSubmit = message.trim().length > 0 && !loading;
return (
<React.Fragment>
<DialogTitle >{props.title}</DialogTitle>
<Divider></Divider>
<DialogContent style={{padding: "10px"}}>
Send a feedback report to the slack webhook configured for the current operation.
This provides a way to easily capture a bug, feedback requests, or comments without breaking operator flow too much. <br/>
Shift+Enter will auto-submit the form.
<TableContainer component={Paper} className="mythicElement">
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
<TableBody style={{whiteSpace: "pre"}}>
<TableRow hover >
<TableCell style={{width: "5rem"}}>
Feedback Type
</TableCell>
<TableCell>
<Select
labelId="demo-dialog-select-label"
id="demo-dialog-select"
value={messageType}
onChange={handleMessageTypeChange}
input={<Input style={{width: "100%"}}/>}
>
{messageTypeOptions.map( (opt) => (
<MenuItem value={opt.value} key={opt.value}>{opt.display}</MenuItem>
) )}
</Select>
</TableCell>
</TableRow>
<TableRow hover >
<TableCell style={{width: "5rem"}}>
Task (if applicable)
</TableCell>
<TableCell>
<MythicTextField value={taskID} type={"number"}
onChange={handleTaskIDChange} display="inline-block" name={"taskid"} showLabel={false}
/>
</TableCell>
</TableRow>
<TableRow hover>
<TableCell>
Feedback
</TableCell>
<TableCell>
<MythicTextField value={message} multiline={true} onEnter={handleOnEnter}
onChange={handleMessageChange} display="inline-block" name={"taskid"} showLabel={false}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<DialogTitle>{props.title || "Submit Feedback"}</DialogTitle>
<DialogContent dividers={true}>
<MythicDialogBody>
<MythicDialogSection
title="Feedback Destination"
description="Send this report to the feedback webhook configured for the current operation."
>
<MythicFormNote>
Use this for bugs, workflow friction, feature ideas, detection notes, or anything operators should be able to capture without leaving Mythic.
</MythicFormNote>
</MythicDialogSection>
<MythicDialogSection
title="Report Details"
description="Capture the category, optional task context, and the feedback details."
>
<MythicForm onSubmit={onSubmitForm}>
<MythicDialogGrid minWidth="14rem">
<MythicFormField label="Feedback Type" description={selectedMessageType?.description} required>
<FormControl fullWidth size="small">
<Select
labelId="feedback-type-select-label"
id="feedback-type-select"
value={messageType}
onChange={handleMessageTypeChange}
>
{messageTypeOptions.map( (opt) => (
<MenuItem value={opt.value} key={opt.value}>{opt.display}</MenuItem>
) )}
</Select>
</FormControl>
</MythicFormField>
<MythicFormField
label="Task"
description="Optional task display ID for feedback tied to specific output or tasking."
>
<TextField
fullWidth
size="small"
type="number"
value={taskID}
onChange={handleTaskIDChange}
placeholder="Optional"
inputProps={{min: 0}}
/>
</MythicFormField>
</MythicDialogGrid>
<MythicFormField
label="Feedback"
description="Include the context an operator or maintainer would need to understand the issue."
required
>
<TextField
autoFocus
fullWidth
multiline
minRows={6}
maxRows={10}
value={message}
onChange={handleMessageChange}
placeholder="What happened, what did you expect, and what would make this easier?"
/>
</MythicFormField>
</MythicForm>
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
<Button onClick={handleSubmit} variant="contained" color="success">
Submit Feedback
</Button>
</DialogActions>
</MythicDialogButton>
<MythicDialogButton onClick={handleSubmit} intent="primary" disabled={!canSubmit}>
{loading ? "Submitting..." : "Submit Feedback"}
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
);
}
@@ -9,17 +9,10 @@ import ComputerIcon from '@mui/icons-material/Computer';
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 { Typography } from '@mui/material';
import { MythicStyledTooltip } from "./MythicStyledTooltip";
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 WidgetsIcon from '@mui/icons-material/Widgets';
import ListSubheader from '@mui/material/ListSubheader';
import { areEqual } from 'react-window';
const PREFIX = 'FileBrowserVirtualTree';
@@ -46,31 +39,65 @@ const StyledAutoSizer = styled(AutoSizer)((
theme
}
) => ({
[`& .${classes.rowContainer}`]: {},
height: "100%",
minHeight: 0,
minWidth: 0,
overflow: "hidden",
width: "100%",
[`& .${classes.rowContainer}`]: {
cursor: "pointer",
overflow: "hidden",
transition: "background-color 120ms ease, color 120ms ease",
},
[`& .${classes.rowContainer}:hover`]: {
backgroundColor: theme.palette.action.hover,
},
[`& .${classes.row}`]: {
display: "flex",
alignItems: "left",
marginLeft: (props) => theme.spacing(3 * props.depth),
alignItems: "center",
height: "100%",
minWidth: "fit-content",
paddingRight: theme.spacing(1),
userSelect: "none",
whiteSpace: "nowrap"
whiteSpace: "nowrap",
width: "100%",
},
[`& .${classes.row}[data-deleted="true"]`]: {
textDecoration: "line-through",
},
[`& .${classes.rowButtonWrapper}`]: {
width: theme.spacing(3),
textAlign: "center",
"&:hover": {
cursor: "pointer",
textDecoration: "underline"
}
alignSelf: "stretch",
borderLeft: `2px dashed ${alpha(theme.palette.text.primary, 0.34)}`,
flex: "0 0 14px",
marginLeft: 7,
},
[`& .${classes.rowButton}`]: {
width: theme.spacing(3)
alignItems: "center",
display: "inline-flex",
flex: "0 0 auto",
justifyContent: "center",
width: theme.spacing(3),
},
[`& .${classes.rowLabel}`]: {
marginLeft: theme.spacing(0.5)
color: theme.palette.text.primary,
fontFamily: "inherit",
fontSize: theme.typography.pxToRem(13),
lineHeight: 1.35,
margin: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
},
[`& .${classes.rowLabel}[data-muted="true"]`]: {
color: theme.palette.text.secondary,
},
[`& .${classes.heading}`]: {
@@ -108,9 +135,20 @@ const StyledAutoSizer = styled(AutoSizer)((
},
[`& .${classes.icon}`]: {
flex: "0 0 auto",
height: 16,
marginLeft: 3,
marginRight: 5,
verticalAlign: 'middle',
height: 20,
width: 20,
width: 16,
},
[`& .${classes.icon}[data-folder="true"]`]: {
color: theme.folderColor,
},
[`& .${classes.icon}[data-empty="true"]`]: {
color: theme.emptyFolderColor,
},
[`& .${classes.details}`]: {
@@ -126,7 +164,11 @@ const StyledAutoSizer = styled(AutoSizer)((
[`& .${classes.paper}`]: {
width: '100%',
marginBottom: theme.spacing(2),
height: "100%",
marginBottom: 0,
minHeight: 0,
minWidth: 0,
overflow: "hidden",
},
[`& .${classes.table}`]: {
@@ -145,22 +187,31 @@ const StyledAutoSizer = styled(AutoSizer)((
width: 1,
}
}));
const VirtualTreeRow = ({
function itemKey(index, data) {
// Find the item at the specified index.
// In this case "data" is an Array that was passed to List as "itemData".
const item = data[index];
if(item.root){
return `${item.group};${item.id}`;
}
if(item.is_group){
return item.group;
}
return `${item.group};${item.host};${item.full_path_text}`;
}
const VirtualTreeRow = React.memo(({
onSelectNode,
onExpandNode,
onCollapseNode,
onDoubleClickNode,
contextMenuOptions,
onContextMenu,
tabInfo,
selectedFolderData,
...ListProps
}) => {
const itemTreeData = ListProps.data[ListProps.index];
const item = ListProps.treeRootData[itemTreeData.group]?.[itemTreeData.host]?.[itemTreeData.full_path_text] || itemTreeData;
//console.log("item", item, "itemlookup", ListProps.treeRootData[itemTreeData.host]?.[itemTreeData.name])
const dropdownAnchorRef = React.useRef(null);
const theme = useTheme();
const handleOnClickButton = (e) => {
e.stopPropagation();
if (itemTreeData.isOpen) {
@@ -173,167 +224,97 @@ const VirtualTreeRow = ({
const handleOnClickRow = (e) => {
onSelectNode(item.id, {...item, group: itemTreeData.group, host: itemTreeData.host});
};
const [openContextMenu, setOpenContextMenu] = React.useState(false);
const handleContextClick = useCallback(
(event) => {
event.preventDefault();
event.stopPropagation();
if(item.root){
}else if(item.is_group){
}else {
if(contextMenuOptions && contextMenuOptions.length > 0){
setOpenContextMenu(true);
}
const handleContextClick = (e) => {
onContextMenu({event: e, item, itemTreeData});
}
const selectedPath = () => {
if(itemTreeData.group === selectedFolderData.group && itemTreeData.host === selectedFolderData.host){
if(itemTreeData.root){
return "selectedCallbackHierarchy";
}
if(selectedFolderData.full_path_text === itemTreeData.full_path_text){
return "selectedCallback";
}
},
[contextMenuOptions] // eslint-disable-line react-hooks/exhaustive-deps
);
const handleMenuItemClick = (event, index, callback_id, callback_display_id) => {
event.preventDefault();
event.stopPropagation();
contextMenuOptions[index].click({event,
node: {...item, group: itemTreeData.group, host: itemTreeData.host},
callback_id, callback_display_id
});
setOpenContextMenu(false);
};
const handleClose = (event) => {
if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
return;
}
setOpenContextMenu(false);
};
return "";
}
return (
<div className={"hoverme"} style={ListProps.style}>
<div style={{display: 'flex' , marginBottom: "1px", flexGrow: 1, width: "100%"}}>
<div className={`${classes.rowContainer} hoverme ${selectedPath()}`.trim()}
style={ListProps.style}
onContextMenu={handleContextClick}
onClick={handleOnClickRow}>
<div className={classes.row} data-deleted={itemTreeData.deleted ? "true" : undefined}>
{[...Array(itemTreeData.depth)].map((o, i) => (
<div
<span
className={classes.rowButtonWrapper}
key={'folder' + itemTreeData.id + 'lines' + i}
style={{
borderLeft: `2px dashed ${alpha(theme.palette.text.primary, 0.4)}`,
marginLeft: 7,
paddingRight: 7,
display: 'inline-block',
}}></div>
/>
))}
<div
className={classes.root}
style={{ backgroundColor: theme.body, color: theme.text, alignItems: 'center', display: 'flex', paddingRight: "10px", textDecoration: itemTreeData.deleted ? 'line-through' : '' }}
onClick={handleOnClickRow}
onContextMenu={handleContextClick}
ref={dropdownAnchorRef}
>
<Popper open={openContextMenu} anchorEl={dropdownAnchorRef.current} role={undefined} transition disablePortal style={{zIndex: 4}}>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
}}
>
<Paper variant="outlined" className={"dropdownMenuColored"}>
<ClickAwayListener onClickAway={handleClose}>
<MenuList id="split-button-menu" style={{paddingTop: 0}} >
<ListSubheader component={"li"} className={"MuiListSubheader-root"}>
Act from current Callback: {tabInfo["displayID"]}
</ListSubheader>
{contextMenuOptions.map((option, index) => (
<MenuItem
key={option.name + index}
onClick={(event) => handleMenuItemClick(event, index, tabInfo["callbackID"], tabInfo["displayID"])}
>
{option.name}
</MenuItem>
))}
{
item?.callback && item?.["callback"]?.["id"] !== tabInfo["callbackID"] &&
<ListSubheader component={"li"} className={"MuiListSubheader-root"}>
Act from originating Callback: {item?.callback?.["display_id"] || tabInfo["displayID"]}
</ListSubheader>
}
{
item?.callback && item?.["callback"]?.["id"] !== tabInfo["callbackID"] &&
contextMenuOptions.map((option, index) => (
<MenuItem
key={option.name + index}
onClick={(event) => handleMenuItemClick(event, index,
item?.["callback"]?.["id"] || tabInfo["callbackID"],
item?.["callback"]?.["display_id"] || tabInfo["displayID"])}
>
{option.name}
</MenuItem>
))
}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
{itemTreeData.is_group ? (
<WidgetsIcon style={{marginLeft: "3px", marginRight: "5px" }} />
<WidgetsIcon className={classes.icon} />
): itemTreeData.root ? (
<ComputerIcon style={{ marginLeft: '3px', marginRight: '5px' }} />
<ComputerIcon className={classes.icon} />
) : !itemTreeData.can_have_children ? (
<DescriptionIcon style={{ marginLeft: '3px', marginRight: '5px' }} />
<DescriptionIcon className={classes.icon} />
) : itemTreeData.isOpen ? (
<FontAwesomeIcon
icon={faFolderOpen}
style={{
marginLeft: '3px',
marginRight: '5px',
color: item?.metadata?.has_children ? theme.folderColor : theme.palette.text.secondary,
}}
size={"1x"}
className={classes.icon}
data-folder={item?.has_children || item.success ? "true" : undefined}
data-empty={item?.has_children || item.success ? undefined : "true"}
size={"lg"}
onClick={handleOnClickButton} />
) : (
<FontAwesomeIcon
style={{ paddingTop: '5px', marginLeft: '3px', marginRight: '5px', color: item?.metadata?.has_children ? theme.folderColor : theme.palette.text.secondary, }}
size={"lg"} icon={faFolder} onClick={handleOnClickButton} />
className={classes.icon}
data-folder={item?.has_children || item.success ? "true" : undefined}
data-empty={item?.has_children || item.success ? undefined : "true"}
size={"lg"}
icon={faFolder} onClick={handleOnClickButton} />
)}
<Typography
style={{
color:
item?.metadata?.has_children ||
item.success !== null
? theme.palette.text.primary
: theme.palette.text.secondary,
}} component="pre">
className={classes.rowLabel}
data-muted={item?.has_children || item.success !== null ? undefined : "true"}
component="pre">
{itemTreeData.name}
</Typography>
{item.success === true && itemTreeData.depth > 0 ? (
<MythicStyledTooltip title='Successfully listed contents of folder'>
<CheckCircleOutlineIcon fontSize='small' color="success" />
<CheckCircleOutlineIcon className={classes.icon} fontSize='small' color="success" />
</MythicStyledTooltip>
) : item.success === false && itemTreeData.depth > 0 ? (
<MythicStyledTooltip title='Failed to list contents of folder'>
<ErrorIcon fontSize='small' color="error" />
<ErrorIcon className={classes.icon} fontSize='small' color="error" />
</MythicStyledTooltip>
) : null}
</div>
</div>
</div>
);
};
const FileBrowserVirtualTree = ({
}, areEqual);
const caseInsensitiveCompare = (a, b) => {
try{
return a.localeCompare(b);
}catch(error){
console.log("localeCompare failed for", a, b);
return a < b;
}
}
const FileBrowserVirtualTreePreMemo = ({
treeRootData,
treeAdjMatrix,
openNodes,
onSelectNode,
onExpandNode,
onCollapseNode,
contextMenuOptions,
onContextMenu,
showDeletedFiles,
selectedFolderData,
tabInfo,
}) => {
const gridRef = React.useRef(null);
const flattenNode = useCallback(
// node is just a full_path_text
(node, group, host, depth = 0) => {
@@ -341,26 +322,26 @@ const FileBrowserVirtualTree = ({
if(depth === 0){
return [
{
id: treeRootData[group][host][node].id,
name: treeRootData[group][host][node].name_text,
full_path_text: treeRootData[group][host][node].full_path_text,
deleted: treeRootData[group][host][node].deleted,
id: treeRootData[group]?.[host]?.[node].id,
name: treeRootData[group]?.[host]?.[node].name_text,
full_path_text: treeRootData[group]?.[host]?.[node].full_path_text,
deleted: treeRootData[group]?.[host]?.[node].deleted,
depth,
isLeaf: Object.keys(treeAdjMatrix[group][host]?.[node] || {}).length === 0,
can_have_children: treeRootData[group][host][node].can_have_children,
isLeaf: Object.keys(treeAdjMatrix[group]?.[host]?.[node] || {}).length === 0,
can_have_children: treeRootData[group]?.[host]?.[node].can_have_children,
isOpen: true,
children: (treeAdjMatrix[group][host]?.[node] || {}),
host,
group,
root: true
},
...(Object.keys(treeAdjMatrix[group][host]?.[node] || {})).reduce( (prev, cur) => {
...(Object.keys(treeAdjMatrix[group]?.[host]?.[node] || {})).sort(caseInsensitiveCompare).reduce( (prev, cur) => {
if(!treeRootData[group][host][cur].can_have_children){return [...prev]}
return [...prev, flattenNode(cur, group, host, depth+1)];
}, []).flat()
];
}
if (openNodes[treeRootData[group][host][node].id] === true) {
if (openNodes[`${group};${host};${treeRootData[group][host][node].full_path_text}`] === true) {
return [
{
id: treeRootData[group][host][node].id,
@@ -368,15 +349,15 @@ const FileBrowserVirtualTree = ({
full_path_text: treeRootData[group][host][node].full_path_text,
deleted: treeRootData[group][host][node].deleted,
depth,
isLeaf: Object.keys(treeAdjMatrix[group][host]?.[node] || {}).length === 0,
isLeaf: Object.keys(treeAdjMatrix[group]?.[host]?.[node] || {}).length === 0,
can_have_children: treeRootData[group][host][node].can_have_children,
isOpen: true,
children: (treeAdjMatrix[group][host]?.[node] || {}),
children: (treeAdjMatrix[group]?.[host]?.[node] || {}),
host,
group,
root: false,
},
...(Object.keys(treeAdjMatrix[group][host]?.[node] || {})).reduce( (prev, cur) => {
...(Object.keys(treeAdjMatrix[group]?.[host]?.[node] || {})).sort(caseInsensitiveCompare).reduce( (prev, cur) => {
if(!treeRootData[group][host][cur].can_have_children){return [...prev]}
if(!showDeletedFiles && treeRootData[group][host][cur].deleted){return [...prev]}
return [...prev, flattenNode(cur, group, host, depth+1)];
@@ -385,13 +366,13 @@ const FileBrowserVirtualTree = ({
}
return [
{
id: treeRootData[group][host][node].id,
name: treeRootData[group][host][node].name_text,
full_path_text: treeRootData[group][host][node].full_path_text,
deleted: treeRootData[group][host][node].deleted,
id: treeRootData[group]?.[host]?.[node].id,
name: treeRootData[group]?.[host]?.[node].name_text,
full_path_text: treeRootData[group]?.[host]?.[node].full_path_text,
deleted: treeRootData[group]?.[host]?.[node].deleted,
depth,
isLeaf: Object.keys(treeAdjMatrix[group]?.[host]?.[node] || {}).length === 0,
can_have_children: treeRootData[group][host][node].can_have_children,
can_have_children: treeRootData[group]?.[host]?.[node].can_have_children,
isOpen: false,
children: (treeAdjMatrix[group]?.[host]?.[node] || {}),
host,
@@ -401,15 +382,14 @@ const FileBrowserVirtualTree = ({
];
},
[openNodes, showDeletedFiles] // eslint-disable-line react-hooks/exhaustive-deps
[openNodes, showDeletedFiles, treeAdjMatrix] // eslint-disable-line react-hooks/exhaustive-deps
);
const flattenedNodes = useMemo(() => {
//console.log("in tree", treeRootData, treeAdjMatrix)
// need to return an array
let finalData = [];
//console.log(treeAdjMatrix);
const groupKeys = Object.keys(treeAdjMatrix).sort();
const groupKeys = Object.keys(treeAdjMatrix).sort(caseInsensitiveCompare);
for(let i = 0; i < groupKeys.length; i++){
finalData.push({
id: groupKeys[i],
@@ -426,7 +406,7 @@ const FileBrowserVirtualTree = ({
children: treeAdjMatrix[groupKeys[i]],
full_path_text: groupKeys[i],
});
const hostKeys = Object.keys(treeAdjMatrix[groupKeys[i]]).sort();
const hostKeys = Object.keys(treeAdjMatrix[groupKeys[i]]).sort(caseInsensitiveCompare);
for(let j = 0; j < hostKeys.length; j++){
//for(const [host, matrix] of Object.entries(hosts)){
finalData.push({
@@ -444,7 +424,7 @@ const FileBrowserVirtualTree = ({
full_path_text: hostKeys[j],
});
//console.log(matrix);
finalData.push(...Object.keys(treeAdjMatrix[groupKeys[i]][hostKeys[j]][""]).reduce((prev, c) => {
finalData.push(...Object.keys(treeAdjMatrix[groupKeys[i]][hostKeys[j]][""]).sort(caseInsensitiveCompare).reduce((prev, c) => {
if(!showDeletedFiles && c.deleted) {
return [...prev];
} else {
@@ -453,37 +433,51 @@ const FileBrowserVirtualTree = ({
}, []).flat())
}
}
//console.log("flattened data", finalData)
return finalData;
//nodes.map((node) => flattenNode(node)).flat()
},[flattenNode, treeRootData, treeAdjMatrix, showDeletedFiles]);
React.useEffect( () => {
let rowIndex = flattenedNodes?.findIndex(e =>
e.full_path_text === selectedFolderData.full_path_text &&
e.host === selectedFolderData.host &&
e.group === selectedFolderData.group
);
if(rowIndex >= 0){
if(gridRef.current){
gridRef.current?.scrollToItem(rowIndex, "smart")
}
}
}, [selectedFolderData, flattenedNodes]);
return flattenedNodes.length > 0 ? (
<StyledAutoSizer>
{(AutoSizerProps) => (
<List
itemData={flattenedNodes}
direction="vertical"
height={AutoSizerProps.height}
width={AutoSizerProps.width}
itemCount={flattenedNodes.length}
itemSize={24}
>
{(ListProps) => (
<VirtualTreeRow
{...ListProps}
tabInfo={tabInfo}
treeRootData={treeRootData}
onSelectNode={onSelectNode}
onExpandNode={onExpandNode}
onCollapseNode={onCollapseNode}
contextMenuOptions={contextMenuOptions}
/>
)}
</List>
)}
</StyledAutoSizer>
<div className="mythic-process-browser-table-shell flex flex-column h-full min-h-0 min-w-0 overflow-hidden w-full relative">
<StyledAutoSizer>
{(AutoSizerProps) => (
<List
itemData={flattenedNodes}
layout="vertical"
height={AutoSizerProps.height}
width={AutoSizerProps.width}
itemCount={flattenedNodes.length}
itemKey={itemKey}
itemSize={24}
ref={gridRef}
>
{(ListProps) => (
<VirtualTreeRow
{...ListProps}
tabInfo={tabInfo}
selectedFolderData={selectedFolderData}
treeRootData={treeRootData}
onSelectNode={onSelectNode}
onExpandNode={onExpandNode}
onCollapseNode={onCollapseNode}
onContextMenu={onContextMenu}
/>
)}
</List>
)}
</StyledAutoSizer>
</div>
) : null;
};
export default FileBrowserVirtualTree;
export const FileBrowserVirtualTree = React.memo(FileBrowserVirtualTreePreMemo);
@@ -1,122 +1,102 @@
import React from 'react';
import {previewFileQuery} from "../pages/Search/FileMetaTable";
import {snackActions} from "../utilities/Snackbar";
import {MythicStyledTooltip} from "./MythicStyledTooltip";
import hexFile from "../../assets/file_bin.png";
import txtFile from "../../assets/file_txt.png";
import {MythicDialog} from "./MythicDialog";
import {PreviewFileStringDialog} from "../pages/Search/PreviewFileStringDialog";
import {PreviewFileHexDialog} from "../pages/Search/PreviewFileHexDialog";
import {Link} from '@mui/material';
import {useMutation, useLazyQuery, gql} from '@apollo/client';
import {gql} from '@apollo/client';
import {b64DecodeUnicode} from "../pages/Callbacks/ResponseDisplay";
import {PreviewFileMediaDialog} from "./PreviewFileMedia";
import {faPhotoVideo} from '@fortawesome/free-solid-svg-icons';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {useMythicLazyQuery} from "../utilities/useMythicLazyQuery";
import {FileDownloadLinkWithAuth} from "../utilities/FileDownloadWithAuth";
export const getfileInformationQuery = gql`
query getFileInformation($file_id: String!){
filemeta(where: {agent_file_id: {_eq: $file_id}}){
filename_text
agent_file_id
}
}
`;
export const MythicFileContext = ({agent_file_id, display_link, filename}) => {
export const MythicFileContext = ({agent_file_id, display_link, filename, extraStyles}) => {
const [fileData, setFileData] = React.useState({
agent_file_id: agent_file_id,
display_link: display_link,
filename: filename,
})
const [openPreviewStringsDialog, setOpenPreviewStringsDialog] = React.useState(false);
const [openPreviewHexDialog, setOpenPreviewHexDialog] = React.useState(false);
const [fileContents, setFileContents] = React.useState('');
const [previewFileString] = useMutation(previewFileQuery, {
onCompleted: (data) => {
if(data.previewFile.status === "success"){
setFileContents(data.previewFile.contents);
setOpenPreviewStringsDialog(true);
}else{
snackActions.error(data.previewFile.error)
}
},
onError: (data) => {
console.log(data);
snackActions.error(data);
}
});
const [previewFileHex] = useMutation(previewFileQuery, {
onCompleted: (data) => {
if(data.previewFile.status === "success"){
setFileContents(data.previewFile.contents);
setOpenPreviewHexDialog(true);
}else{
snackActions.error(data.previewFile.error)
}
},
onError: (data) => {
console.log(data);
snackActions.error(data);
}
});
const onPreviewStrings = (event) => {
const [openPreviewMediaDialog, setOpenPreviewMediaDialog] = React.useState(false);
const onPreviewMedia = (event) => {
if(event){
event.preventDefault();
event.stopPropagation();
}
previewFileString({variables: {file_id: fileData.agent_file_id}})
setOpenPreviewMediaDialog(true);
}
const onPreviewHex = (event) => {
if(event){
event.preventDefault();
event.stopPropagation();
const getFileInformationSuccess = (data) => {
if(data.filemeta.length === 0){
setFileData({
filename: undefined,
display_link: "",
agent_file_id: agent_file_id,
})
return;
}
setFileData( {...fileData,
agent_file_id: data.filemeta[0].agent_file_id,
filename: b64DecodeUnicode(data.filemeta[0].filename_text)});
if(display_link === "" || display_link === undefined){
setFileData( {...fileData,
agent_file_id: data.filemeta[0].agent_file_id,
filename: b64DecodeUnicode(data.filemeta[0].filename_text),
display_link: b64DecodeUnicode(data.filemeta[0].filename_text)});
}
previewFileHex({variables: {file_id: fileData.agent_file_id}})
}
const [getFileInformation] = useLazyQuery(getfileInformationQuery, {
onCompleted: (data) => {
setFileData( {...fileData, filename: b64DecodeUnicode(data.filemeta[0].filename_text)});
if(display_link === "" || display_link === undefined){
setFileData( {...fileData, display_link: b64DecodeUnicode(data.filemeta[0].filename_text)});
}
},
onError: (data) => {
snackActions.error("Failed to fetch instance data: " + data);
console.log(data);
},
const getFileInformationError = (data) => {
snackActions.error("Failed to fetch file data: " + data);
console.log(data);
}
const getFileInformation = useMythicLazyQuery(getfileInformationQuery, {
fetchPolicy: "no-cache"
})
React.useEffect( () => {
if(filename === "" || filename === undefined){
getFileInformation({variables: {file_id: fileData.agent_file_id}})
if(agent_file_id !== '' && agent_file_id !== undefined){
getFileInformation({variables: {file_id: agent_file_id}})
.then(({data}) => getFileInformationSuccess(data)).catch((data) => getFileInformationError(data));
}
}, [filename]);
}, [filename, agent_file_id]);
if(agent_file_id === '' || agent_file_id === undefined){
return null
}
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 HEX XXD"}>
<img src={hexFile} alt={"preview hex"} style={{height: "35px", cursor: "pointer"}}
onClick={onPreviewHex}/>
<MythicStyledTooltip title={"Preview Media"} tooltipStyle={extraStyles ? extraStyles : {}}>
<FontAwesomeIcon icon={faPhotoVideo}
style={{height: "20px", position: "relative", cursor: "pointer", display: "inline-block"}}
onClick={onPreviewMedia} />
</MythicStyledTooltip>
<MythicStyledTooltip title={"Preview Strings"}>
<img src={txtFile} alt={"preview strings"} style={{height: "35px", cursor: "pointer"}}
onClick={onPreviewStrings} />
</MythicStyledTooltip>
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" href={"/direct/download/" + fileData.agent_file_id}>
<FileDownloadLinkWithAuth style={{wordBreak: "break-all"}} color="textPrimary" underline="always" href={"/direct/download/" + fileData.agent_file_id}>
{fileData.display_link === "" ? window.location.origin + "/direct/download/" + fileData.agent_file_id : fileData.display_link}
</Link>
{openPreviewStringsDialog &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openPreviewStringsDialog}
onClose={()=>{setOpenPreviewStringsDialog(false);}}
innerDialog={<PreviewFileStringDialog onClose={()=>{setOpenPreviewStringsDialog(false);}}
filename={fileData.filename} contents={fileContents}
/>}
/>
}
{openPreviewHexDialog &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openPreviewHexDialog}
onClose={()=>{setOpenPreviewHexDialog(false);}}
innerDialog={<PreviewFileHexDialog onClose={()=>{setOpenPreviewHexDialog(false);}}
filename={fileData.filename} contents={fileContents}
/>}
</FileDownloadLinkWithAuth>
{openPreviewMediaDialog &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openPreviewMediaDialog}
onClose={onClose}
innerDialog={<PreviewFileMediaDialog
agent_file_id={fileData.agent_file_id}
filename={fileData.filename}
onClose={onClose} />}
/>
}
</>
)
}
}
@@ -1,24 +1,33 @@
import { snackActions } from '../utilities/Snackbar';
import {snackActions} from '../utilities/Snackbar';
import {mythicFetch} from '../utilities/MythicConnection';
export const UploadTaskFile = async (file, comment) => {
let formData = new FormData();
formData.append("file", file);
formData.append("comment", comment);
snackActions.info("Uploading " + file.name + " to Mythic...", {autoHideDuration: 1000});
try{
const upload_response = await fetch('/api/v1.4/task_upload_file_webhook', {
formData.append("file", file);
formData.append("comment", comment);
snackActions.info("Uploading " + file.name + " to Mythic...", {autoHideDuration: 1000});
}catch(error){
console.log(error)
return null;
}
try{
const upload_response = await mythicFetch('/task_upload_file_webhook', {
method: 'POST',
body: formData,
headers: {
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
"Authorization": `Bearer ${localStorage.getItem("access_token")}`,
MythicSource: "web",
}
});
try{
const upload_result = upload_response.json().then(data => {
//console.log(data);
return data?.agent_file_id || data?.error || null;
if(data?.status === "error"){
snackActions.error(data.error || "Failed to upload file");
return null;
}
return data?.agent_file_id || null;
}).catch(error => {
console.log(upload_response);
snackActions.warning("Error: " + upload_response.statusText + "\nError Code: " + upload_response.status);
console.log("Error trying to get json response", error.toString());
return null;
@@ -32,4 +41,108 @@ export const UploadTaskFile = async (file, comment) => {
snackActions.error(error.toString());
return null;
}
}
}
export const UploadDirectFile = async (agent_file_id, file) => {
let formData = new FormData();
try{
formData.append("file", file);
//snackActions.info("Saving " + file.name + " to Mythic...", {autoHideDuration: 1000});
}catch(error){
console.log(error)
return null;
}
try{
const upload_response = await mythicFetch('/direct/upload/' + agent_file_id, {
method: 'POST',
body: formData,
headers: {
"Authorization": `Bearer ${localStorage.getItem("access_token")}`,
MythicSource: "web",
}
});
try{
return upload_response.json().then(data => {
if(data?.status === "error"){
snackActions.error(data.error || "Failed to save file");
return data;
}
return data;
}).catch(error => {
snackActions.warning("Error: " + upload_response.statusText + "\nError Code: " + upload_response.status);
console.log("Error trying to get json response", error.toString());
return null;
});
}catch(error){
snackActions.error(error.toString());
return null;
}
}catch(error){
snackActions.error(error.toString());
return null;
}
}
export const UploadEventFile = async (file, comment) => {
let formData = new FormData();
formData.append("file", file);
formData.append("comment", comment);
snackActions.info("Uploading " + file.name + " to Mythic...", {autoHideDuration: 1000});
try{
const upload_response = await mythicFetch('/eventing_import_webhook', {
method: 'POST',
body: formData,
headers: {
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
}
});
try{
return upload_response.json().then(data => {
//console.log(data);
return data;
}).catch(error => {
console.log(upload_response);
snackActions.warning("Error: " + upload_response.statusText + "\nError Code: " + upload_response.status);
console.log("Error trying to get json response", error.toString());
return null;
});
}catch(error){
snackActions.error(error.toString());
return null;
}
}catch(error){
snackActions.error(error.toString());
return null;
}
}
export const UploadEventGroupFile = async (file, eventgroup_id) => {
let formData = new FormData();
formData.append("eventgroup_id", eventgroup_id);
formData.append("file", file);
snackActions.info("Uploading " + file.name + " to Mythic...", {autoHideDuration: 1000});
try{
const upload_response = await mythicFetch('/eventing_register_file_webhook', {
method: 'POST',
body: formData,
headers: {
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
}
});
try{
return upload_response.json().then(data => {
//console.log(data);
return data;
}).catch(error => {
console.log(upload_response);
snackActions.warning("Error: " + upload_response.statusText + "\nError Code: " + upload_response.status);
console.log("Error trying to get json response", error.toString());
return null;
});
}catch(error){
snackActions.error(error.toString());
return null;
}
}catch(error){
snackActions.error(error.toString());
return null;
}
}
@@ -0,0 +1,263 @@
import * as React from "react";
import styled from "@emotion/styled";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import ArrowRight from "@mui/icons-material/ArrowRight";
// slightly modified from https://medium.com/geekculture/creating-a-dropdown-with-nested-menu-items-using-react-mui-bb0c084226da
export const Dropdown = React.forwardRef(
(
{
menu,
isOpen: controlledIsOpen,
onOpen: onControlledOpen,
externallyOpen,
minWidth,
absoluteX,
absoluteY,
anchorReference="anchorEl",
style,
transformOrigin,
anchorOrigin,
MenuListProps,
PaperProps,
},
ref
) => {
const [isInternalOpen, setInternalOpen] = React.useState(null);
const isOpen = controlledIsOpen || isInternalOpen;
let anchorRef = React.useRef(isOpen);
if (ref) {
anchorRef = ref;
}
const handleClose = (event) => {
event.stopPropagation();
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
handleForceClose();
};
const handleForceClose = () => {
onControlledOpen ? onControlledOpen(null) : setInternalOpen(null);
};
const renderMenu = (menuItem, index) => {
const { ...props } = menuItem.props;
let extraProps = {};
if (props.menu) {
extraProps = {
parentMenuOpen: isOpen
};
}
return React.createElement(menuItem.type, {
...props,
key: index,
...extraProps,
onClick: (event) => {
event.stopPropagation();
if (menuItem.props.onClick) {
menuItem.props.onClick(event);
}
},
children: props.menu
? React.Children.map(props.menu, renderMenu)
: props.children
});
};
const anchorEl = isOpen && isOpen.currentTarget ? isOpen.currentTarget : isOpen;
return (
<>
<Menu
elevation={5}
PaperProps={{
...PaperProps,
sx: { minWidth: minWidth ?? 0, ...(PaperProps?.sx || {}) }
}}
MenuListProps={MenuListProps}
style={{zIndex: 100000, position: "absolute"}}
anchorEl={anchorReference === "anchorEl" ? anchorEl : undefined}
transition={"true"}
open={!!externallyOpen}
anchorPosition={anchorReference === "anchorEl" ? undefined : {top: absoluteY, left: absoluteX}}
anchorReference={ anchorReference ? anchorReference : "anchorEl"}
onClose={handleClose}
anchorOrigin={anchorOrigin ? anchorOrigin : {
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={transformOrigin ? transformOrigin : {
vertical: 'top',
horizontal: 'left',
}}
>
{React.Children.map(menu, renderMenu)}
</Menu>
</>
);
}
);
const NestedMenuItem = React.forwardRef((props, ref) => {
const {
parentMenuOpen,
label,
rightIcon = <ArrowRight style={{ fontSize: 16 }} />,
children,
customTheme,
className,
tabIndex: tabIndexProp,
ContainerProps: ContainerPropsProp = {},
rightAnchored,
...MenuItemProps
} = props;
const { ref: containerRefProp, ...ContainerProps } = ContainerPropsProp;
const menuItemRef = React.useRef(null);
React.useImperativeHandle(ref, () => menuItemRef.current);
const containerRef = React.useRef(null);
React.useImperativeHandle(containerRefProp, () => containerRef.current);
const menuContainerRef = React.useRef(null);
const [isSubMenuOpen, setIsSubMenuOpen] = React.useState(false);
const handleMouseEnter = (event) => {
setIsSubMenuOpen(true);
if (ContainerProps?.onMouseEnter) {
ContainerProps.onMouseEnter(event);
}
};
const handleMouseLeave = (event) => {
setIsSubMenuOpen(false);
if (ContainerProps?.onMouseLeave) {
ContainerProps.onMouseLeave(event);
}
};
const isSubmenuFocused = () => {
const active = containerRef.current?.ownerDocument?.activeElement;
for (const child of menuContainerRef.current?.children ?? []) {
if (child === active) {
return true;
}
}
return false;
};
const handleFocus = (event) => {
if (event.target === containerRef.current) {
setIsSubMenuOpen(true);
}
if (ContainerProps?.onFocus) {
ContainerProps.onFocus(event);
}
};
const handleKeyDown = (event) => {
if (event.key === "Escape") {
setIsSubMenuOpen(false);
return;
}
if (isSubmenuFocused()) {
event.stopPropagation();
}
const active = containerRef.current?.ownerDocument?.activeElement;
if (event.key === "ArrowLeft" && isSubmenuFocused()) {
containerRef.current?.focus();
}
if (
event.key === "ArrowRight" &&
event.target === containerRef.current &&
event.target === active
) {
const firstChild = menuContainerRef.current?.children[0];
firstChild?.focus();
}
if(event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "ArrowLeft"){
setIsSubMenuOpen(false);
}
};
const open = isSubMenuOpen && parentMenuOpen;
let tabIndex;
if (!props.disabled) {
tabIndex = tabIndexProp !== undefined ? tabIndexProp : -1;
}
return (
<div
{...ContainerProps}
ref={containerRef}
onFocus={handleFocus}
tabIndex={tabIndex}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onKeyDown={handleKeyDown}
>
<MenuItem
{...MenuItemProps}
data-open={!!open || undefined}
className={className}
ref={menuItemRef}
>
{label}
<div style={{ flexGrow: 1 }} />
{rightIcon}
</MenuItem>
<Menu
hideBackdrop
disablePortal
disableEnforceFocus={false}
disableAutoFocus={false}
style={{ pointerEvents: "none", zIndex: 100000}}
anchorEl={menuItemRef.current}
anchorOrigin={{
vertical: "top",
horizontal: rightAnchored ? "left" : "right"
}}
transformOrigin={{
vertical: "top",
horizontal: rightAnchored ? "right" : "left"
}}
css={customTheme}
open={!!open}
onClose={() => {
setIsSubMenuOpen(false);
}}
MenuListProps={{
'aria-hidden': false,
role: 'menu'
}}
>
<div ref={menuContainerRef} style={{ pointerEvents: "auto" }}>
{children}
</div>
</Menu>
</div>
);
});
export const DropdownMenuItem = styled(MenuItem)`
`;
export const DropdownNestedMenuItem = styled(NestedMenuItem)`
`;
@@ -0,0 +1,16 @@
export const MythicPageBody = ({children}) => {
return (
<div style={{
height: "100%",
display: "flex",
flexDirection: "column",
padding: "0.5rem",
gap: "0.2rem",
minWidth: 0,
minHeight: 0,
}}>
{children}
</div>
)
}
@@ -0,0 +1,270 @@
import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import {alpha, useTheme} from '@mui/material/styles';
import {MythicChip} from './MythicChip';
export const MythicPageHeader = ({
actions,
children,
className = "",
dense = false,
icon,
meta,
style = {},
subtitle,
sx = {},
title,
headerVariant = "h6",
}) => {
const theme = useTheme();
const headerTextColor = theme.pageHeaderText?.main || theme.palette.text.primary;
const supportingTextColor = alpha(headerTextColor, 0.78);
const hasActionContent = actions || children;
const actionSurface = alpha(headerTextColor, 0.1);
const actionSurfaceHover = alpha(headerTextColor, 0.16);
const actionSurfaceActive = alpha(headerTextColor, 0.23);
const actionBorder = alpha(headerTextColor, 0.24);
const actionBorderHover = alpha(headerTextColor, 0.42);
return (
<Paper
className={`mythic-page-header${className ? ` ${className}` : ""}`}
elevation={0}
style={style}
sx={{
alignItems: {xs: "stretch", md: "center"},
backgroundColor: theme.pageHeader.main,
border: `1px solid ${alpha(headerTextColor, 0.18)}`,
borderRadius: `${theme.shape.borderRadius}px`,
boxShadow: `inset 0 1px 0 ${alpha(headerTextColor, 0.12)}`,
color: headerTextColor,
display: "flex",
flex: "0 0 auto",
flexDirection: {xs: "column", md: "row"},
flexWrap: {xs: "nowrap", md: "wrap"},
gap: dense ? 0.75 : 1,
height: "auto",
justifyContent: "space-between",
maxHeight: "none",
minHeight: dense ? 44 : 58,
minWidth: 0,
overflow: "visible",
p: dense ? "0.5rem 0.75rem" : "0.7rem 0.85rem",
width: "100%",
"& .MuiButton-root, & .MuiToggleButton-root": {
alignItems: "center",
backgroundColor: `${actionSurface} !important`,
border: `1px solid ${actionBorder} !important`,
borderRadius: `${theme.shape.borderRadius}px`,
boxShadow: "none !important",
color: `${headerTextColor} !important`,
fontSize: "0.76rem",
fontWeight: 750,
gap: 0.35,
justifyContent: "center",
letterSpacing: 0,
lineHeight: 1.2,
minHeight: 32,
minWidth: {xs: "auto", sm: "7rem"},
px: 1.05,
textTransform: "none",
whiteSpace: "nowrap",
"&:hover": {
backgroundColor: `${actionSurfaceHover} !important`,
borderColor: `${actionBorderHover} !important`,
boxShadow: `inset 0 1px 0 ${alpha(headerTextColor, 0.12)}`,
},
},
"& .MuiButton-root.Mui-focusVisible, & .MuiToggleButton-root.Mui-focusVisible": {
outline: `2px solid ${alpha(headerTextColor, 0.38)}`,
outlineOffset: 2,
},
"& .MuiButton-root.Mui-disabled, & .MuiToggleButton-root.Mui-disabled": {
backgroundColor: `${alpha(headerTextColor, 0.04)} !important`,
borderColor: `${alpha(headerTextColor, 0.1)} !important`,
color: `${alpha(headerTextColor, 0.38)} !important`,
},
"& .MuiToggleButton-root.Mui-selected": {
backgroundColor: `${actionSurfaceActive} !important`,
borderColor: `${alpha(headerTextColor, 0.48)} !important`,
color: `${headerTextColor} !important`,
boxShadow: `inset 0 1px 0 ${alpha(headerTextColor, 0.16)}`,
"&:hover": {
backgroundColor: `${alpha(headerTextColor, 0.28)} !important`,
},
},
"& .MuiButton-startIcon, & .MuiButton-endIcon": {
color: "inherit",
marginLeft: 0,
marginRight: 0,
},
"& .MuiButton-startIcon": {
paddingRight: "0.25rem",
},
"& .MuiButton-endIcon": {
paddingLeft: "0.25rem",
},
"& .MuiIconButton-root": {
backgroundColor: `${actionSurface} !important`,
border: `1px solid ${actionBorder} !important`,
borderRadius: `${theme.shape.borderRadius}px`,
color: `${headerTextColor} !important`,
height: 32,
width: 32,
"&:hover": {
backgroundColor: `${actionSurfaceHover} !important`,
borderColor: `${actionBorderHover} !important`,
},
"&.Mui-disabled": {
backgroundColor: `${alpha(headerTextColor, 0.04)} !important`,
borderColor: `${alpha(headerTextColor, 0.1)} !important`,
color: `${alpha(headerTextColor, 0.38)} !important`,
},
},
...sx,
}}
>
<Stack direction="row" sx={{gap: 1, minWidth: 0, flex: {xs: "0 1 auto", md: "1 1 24rem"}}}>
{icon &&
<Box sx={{
alignItems: "center",
backgroundColor: alpha(headerTextColor, 0.1),
border: `1px solid ${alpha(headerTextColor, 0.16)}`,
borderRadius: `${theme.shape.borderRadius}px`,
display: "inline-flex",
flex: "0 0 auto",
height: dense ? 32 : 36,
justifyContent: "center",
mt: subtitle || meta ? 0.15 : 0,
width: dense ? 32 : 36,
}}>
{icon}
</Box>
}
<Box sx={{display: "flex", flexDirection: "column", gap: 0.25, minWidth: 0}}>
<Typography
variant={headerVariant}
sx={{
color: headerTextColor,
fontWeight: 750,
letterSpacing: 0,
lineHeight: 1.18,
minWidth: 0,
}}
>
{title}
</Typography>
{subtitle &&
<Typography
component="div"
sx={{
color: supportingTextColor,
fontSize: "0.78rem",
fontWeight: 600,
lineHeight: 1.35,
maxWidth: "64rem",
}}
>
{subtitle}
</Typography>
}
{meta &&
<Box sx={{alignItems: "center", display: "flex", flexWrap: "wrap", gap: 0.5, mt: 0.25}}>
{meta}
</Box>
}
</Box>
</Stack>
{hasActionContent &&
<Stack direction="row" sx={{
alignContent: "center",
alignItems: "center",
color: headerTextColor,
flex: "0 1 auto",
flexWrap: "wrap",
gap: 0.45,
justifyContent: {xs: "flex-start", md: "flex-end"},
maxWidth: "100%",
minWidth: 0,
}}>
{actions}
{children}
</Stack>
}
</Paper>
);
}
export const MythicPageHeaderChip = ({status, sx = {}, ...props}) => {
const theme = useTheme();
const headerTextColor = theme.pageHeaderText?.main || theme.palette.text.primary;
const normalizedStatus = status === "active" || status === "enabled" ? "success" :
status === "inactive" || status === "disabled" ? "warning" :
status === "neutral" ? "secondary" : status;
const statusColor = normalizedStatus ? theme.palette[normalizedStatus]?.main : null;
const chipColor = statusColor || alpha(headerTextColor, 0.88);
return (
<MythicChip
size="small"
tone={normalizedStatus || "secondary"}
variant="outlined"
{...props}
sx={{
backgroundColor: statusColor ? alpha(statusColor, theme.palette.mode === "dark" ? 0.22 : 0.13) : alpha(headerTextColor, 0.08),
borderColor: statusColor ? alpha(statusColor, theme.palette.mode === "dark" ? 0.55 : 0.38) : alpha(headerTextColor, 0.2),
color: chipColor,
fontSize: "0.72rem",
fontWeight: 750,
height: 22,
letterSpacing: 0,
"& .MuiChip-icon": {
color: statusColor ? chipColor : alpha(headerTextColor, 0.82),
},
...sx,
}}
/>
);
};
export const MythicSectionHeader = ({
actions,
dense = false,
subtitle,
sx = {},
title,
}) => {
const theme = useTheme();
const headerTextColor = theme.pageHeaderText?.main || theme.palette.text.primary;
const sectionAccentColor = theme.sectionHeader?.accent || theme.palette.primary.main;
return (
<MythicPageHeader
actions={actions}
className="mythic-section-header overflow-hidden relative"
dense={dense}
subtitle={subtitle}
title={title}
sx={{
backgroundImage: theme.gradients?.sectionHeader,
borderColor: alpha(sectionAccentColor, theme.palette.mode === "dark" ? 0.55 : 0.38),
boxShadow: `inset 0 1px 0 ${alpha(headerTextColor, 0.22)}, 0 2px 6px ${alpha(theme.palette.common.black, theme.palette.mode === "dark" ? 0.28 : 0.12)}`,
mb: 0.5,
mt: dense ? 1 : 1.25,
overflow: "hidden",
pl: dense ? 1.45 : 1.6,
position: "relative",
"&::before": {
backgroundColor: sectionAccentColor,
bottom: 0,
boxShadow: `0 0 0 1px ${alpha(headerTextColor, 0.2)}`,
content: '""',
left: 0,
position: "absolute",
top: 0,
width: 6,
},
...sx,
}}
/>
);
};
@@ -1,91 +1,191 @@
import React, { useCallback } from 'react';
import {classes} from './styles';
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 {Dropdown, DropdownMenuItem, DropdownNestedMenuItem} from "../MythicNestedMenus";
const CellPreMemo = ({ VariableSizeGridProps: { style, rowIndex, columnIndex, data } }) => {
const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
const [openContextMenu, setOpenContextMenu] = React.useState(false);
const rowClassName = data.gridUUID + "row" + rowIndex;
const contextMenuOptions = data?.rowContextMenuOptions || [];
const rowHighlight = rowIndex % 2 === 0 ? 'MythicResizableGridRowHighlight' : '';
const [contextMenuOptions, setContextMenuOptions] = React.useState(data?.rowContextMenuOptions || []);
const dropdownAnchorRef = React.useRef(null);
const handleDoubleClick = useCallback(
(e) => {
data.onDoubleClickRow(e, rowIndex - 1); // minus 1 to account for header row
},
[data, rowIndex]
);
const item = data.items[rowIndex][columnIndex];
const cellStyle = item?.props?.cellData?.cellStyle || {};
const rowStyle = data.items[rowIndex][columnIndex]?.props?.rowData?.rowStyle || {};
const onMouseEnter = () => {
const contextMenuLocationRef = React.useRef({x: 0, y: 0});
const ownsContextRowRef = React.useRef(false);
const updateRowClass = useCallback((className, shouldAdd) => {
const cells = document.getElementsByClassName(rowClassName);
if(cells.length > 0){
for(const cell of cells){
cell.classList.add(classes.hoveredRow);
cell.classList.toggle(className, shouldAdd);
}
}
}
const onMouseLeave = () => {
const cells = document.getElementsByClassName(rowClassName);
if(cells.length > 0){
for(const cell of cells){
cell.classList.remove(classes.hoveredRow);
}
}, [rowClassName]);
const handleDoubleClick = useCallback(
(e) => {
e.preventDefault();
e.stopPropagation();
data.onDoubleClickRow(e, rowIndex - 1, data.items[rowIndex][columnIndex]?.props?.rowData); // minus 1 to account for header row
},
[columnIndex, data, rowIndex]
);
const handleClick = (event) => {
event.preventDefault();
event.stopPropagation();
if(data.onRowClick){
data.onRowClick({event, rowDataStatic: data.items[rowIndex][columnIndex]?.props?.rowData})
}
}
const handleMenuItemClick = (event, index) => {
contextMenuOptions[index].click({event, columnIndex, rowIndex, data: data.items[rowIndex][columnIndex]?.props?.rowData || {}});
};
const selectedClass = data.items[rowIndex][columnIndex]?.props?.rowData?.selected ? "selectedCallback" : "";
const customRowClass = data.items[rowIndex][columnIndex]?.props?.rowData?.rowClassName || "";
const rowFirstCellClass = columnIndex === 0 ? classes.rowFirstCell : "";
const rowLastCellClass = columnIndex === data.items[rowIndex].length - 1 ? classes.rowLastCell : "";
const rowInteractiveClass = data.onRowClick ? classes.rowInteractive : "";
const setHoveredRow = useCallback((enabled) => {
updateRowClass(classes.hoveredRow, enabled);
}, [updateRowClass]);
const setContextRow = useCallback((enabled) => {
updateRowClass(classes.contextRow, enabled);
}, [updateRowClass]);
const onMouseEnter = useCallback(() => {
setHoveredRow(true);
}, [setHoveredRow]);
const onMouseLeave = useCallback(() => {
if(!ownsContextRowRef.current){
setHoveredRow(false);
}
}, [setHoveredRow]);
const handleMenuItemClick = (event, clickOption) => {
event.preventDefault();
event.stopPropagation();
clickOption({event, columnIndex, rowIndex, data: data.items[rowIndex][columnIndex]?.props?.rowData || {}});
setOpenContextMenu(false);
};
React.useEffect( () => {
if(openContextMenu){
ownsContextRowRef.current = true;
setContextRow(true);
setHoveredRow(true);
} else if(ownsContextRowRef.current){
ownsContextRowRef.current = false;
setContextRow(false);
setHoveredRow(false);
}
}, [openContextMenu, setContextRow, setHoveredRow]);
React.useEffect( () => {
return () => {
if(ownsContextRowRef.current){
setContextRow(false);
setHoveredRow(false);
}
};
}, [setContextRow, setHoveredRow]);
const handleContextClick = useCallback(
(event) => {
event.preventDefault();
if(item.disableFilterMenu){
event.stopPropagation();
if(item?.disableFilterMenu){
return;
}
if(contextMenuOptions && contextMenuOptions.length > 0){
setOpenContextMenu(true);
contextMenuLocationRef.current.x = event.clientX;
contextMenuLocationRef.current.y = event.clientY;
if(data.onRowContextMenuClick){
const newMenuItems = data.onRowContextMenuClick({rowDataStatic: data.items[rowIndex][columnIndex]?.props?.rowData});
Promise.resolve(newMenuItems).then(function(value) {
if(value.length > 0){
setContextMenuOptions(value);
setOpenContextMenu(true);
}
})
} else {
if(contextMenuOptions && contextMenuOptions.length > 0){
setOpenContextMenu(true);
}
}
},
[contextMenuOptions] // eslint-disable-line react-hooks/exhaustive-deps
[contextMenuOptions, data.onRowContextMenuClick] // eslint-disable-line react-hooks/exhaustive-deps
);
return (
<div style={{...style, ...cellStyle, ...rowStyle}}
className={`${classes.cell} ${rowClassName}`}
className={`${classes.cell} ${rowClassName} ${rowHighlight} ${selectedClass} ${customRowClass} ${rowFirstCellClass} ${rowLastCellClass} ${rowInteractiveClass}`}
onDoubleClick={handleDoubleClick}
onClick={handleClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onContextMenu={handleContextClick}
ref={dropdownAnchorRef}
>
<div className={classes.cellInner}>
<div className={classes.cellInner} style={{height: style.height}}>
{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}) => {
const theme = useTheme();
export const ContextMenu = ({openContextMenu, dropdownAnchorRef, contextMenuOptions, setOpenContextMenu, handleMenuItemClick, contextMenuLocationRef}) => {
const handleClose = (event) => {
if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
return;
}
//if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
// return;
//}
setOpenContextMenu(false);
};
return (
openContextMenu &&
<Popper open={openContextMenu} anchorEl={dropdownAnchorRef.current} role={undefined} transition disablePortal style={{zIndex: 4}}>
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
<Dropdown
isOpen={dropdownAnchorRef.current}
onOpen={setOpenContextMenu}
externallyOpen={openContextMenu}
absoluteX={contextMenuLocationRef.current.x}
absoluteY={contextMenuLocationRef.current.y}
anchorReference={"anchorPosition"}
menu={[
contextMenuOptions.map((option, index) => (
option.type === 'item' ? (
<DropdownMenuItem
key={option.name}
disabled={option.disabled}
className={option.className || (option.danger ? "mythic-action-tone-hover mythic-tone-error" : undefined)}
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}
className={menuOption.className || (menuOption.danger ? "mythic-action-tone-hover mythic-tone-error" : undefined)}
onClick={(event) => handleMenuItemClick(event, menuOption.click)}
>
{menuOption.icon}{menuOption.name}
</DropdownMenuItem>
))
}
/>
) : null
)),
]}
/>
</ClickAwayListener>
)
}
const Cell = React.memo(CellPreMemo);
export default Cell;
/*
<Popper open={openContextMenu} anchorEl={dropdownAnchorRef.current} role={undefined} transition disablePortal style={{zIndex: 4}}>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
@@ -110,8 +210,4 @@ const ContextMenu = ({openContextMenu, dropdownAnchorRef, contextMenuOptions, se
</Grow>
)}
</Popper>
)
}
const Cell = React.memo(CellPreMemo);
export default Cell;
*/
@@ -1,57 +0,0 @@
import React, { useState } from 'react';
import {classes} from './styles';
import Draggable from 'react-draggable';
const DraggableHandles = React.forwardRef(({ height, rowHeight, width, minColumnWidth, columnWidths, onStop }, ref) => {
const [isDragging, setIsDragging] = useState(false);
return (
<div
ref={ref}
className={classes.draggableHandlesContainer}
style={{
height: height,
width: width,
pointerEvents: isDragging ? 'initial' : 'none',
}}>
{columnWidths.map((_, i) => {
const leftOffset = columnWidths.slice(0, i).reduce((a, b) => a + b, 0);
return (
<Draggable
key={i}
axis='x'
bounds={{
left: minColumnWidth - columnWidths[i],
right: Number.POSITIVE_INFINITY,
top: 0,
bottom: 0,
}}
position={isDragging ? null : { x: 0, y: 0 }}
onStart={() => {
setIsDragging(i);
}}
onStop={(e, data) => {
setIsDragging(false);
onStop(data.x, i);
}}>
<div
className={classes.draggableHandlesClickArea}
style={{
left: leftOffset + columnWidths[i] - 1 - 8,
height: rowHeight,
}}>
<div
className={classes.draggableHandlesIndicator}
style={{
display: isDragging === i ? 'block' : 'none',
height: height / 2,
}}
/>
</div>
</Draggable>
);
})}
</div>
);
});
export default DraggableHandles;
@@ -0,0 +1,181 @@
import React from 'react';
import Box from '@mui/material/Box';
import {MythicActionButton} from '../MythicActionButton';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import FormControlLabel from '@mui/material/FormControlLabel';
import Switch from '@mui/material/Switch';
import TextField from '@mui/material/TextField';
import ToggleButton from '@mui/material/ToggleButton';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import Typography from '@mui/material/Typography';
const DEFAULT_FILTER = {
include: [],
exclude: [],
includeMode: "any",
caseSensitive: false,
};
const splitFilterTerms = (value) => {
if(Array.isArray(value)){
return value.map((entry) => String(entry).trim()).filter(Boolean);
}
if(value === undefined || value === null){
return [];
}
return String(value)
.split(/[\n,]+/)
.map((entry) => entry.trim())
.filter(Boolean);
};
export const normalizeGridColumnFilter = (value) => {
if(value === undefined || value === null){
return {...DEFAULT_FILTER};
}
if(typeof value === "string" || typeof value === "number" || typeof value === "boolean"){
return {
...DEFAULT_FILTER,
include: splitFilterTerms(value),
};
}
if(typeof value === "object"){
return {
include: splitFilterTerms(value.include),
exclude: splitFilterTerms(value.exclude),
includeMode: value.includeMode === "all" ? "all" : "any",
caseSensitive: Boolean(value.caseSensitive),
};
}
return {...DEFAULT_FILTER};
};
export const isGridColumnFilterActive = (value) => {
const normalizedFilter = normalizeGridColumnFilter(value);
return normalizedFilter.include.length > 0 || normalizedFilter.exclude.length > 0;
};
export const getUpdatedGridFilterOptions = (filterOptions, key, value) => {
const nextFilterOptions = {...filterOptions};
if(!key){
return nextFilterOptions;
}
const normalizedFilter = normalizeGridColumnFilter(value);
if(isGridColumnFilterActive(normalizedFilter)){
nextFilterOptions[key] = normalizedFilter;
}else{
delete nextFilterOptions[key];
}
return nextFilterOptions;
};
export const gridValuePassesFilter = (value, filterOption) => {
const normalizedFilter = normalizeGridColumnFilter(filterOption);
if(!isGridColumnFilterActive(normalizedFilter)){
return true;
}
const cellText = String(value ?? "");
const haystack = normalizedFilter.caseSensitive ? cellText : cellText.toLowerCase();
const normalizeTerm = (term) => normalizedFilter.caseSensitive ? term : term.toLowerCase();
const termMatches = (term) => haystack.includes(normalizeTerm(term));
const includeMatches = normalizedFilter.include.length === 0 ? true :
normalizedFilter.includeMode === "all" ?
normalizedFilter.include.every(termMatches) :
normalizedFilter.include.some(termMatches);
if(!includeMatches){
return false;
}
return !normalizedFilter.exclude.some(termMatches);
};
export function GridColumnFilterDialog({filterValue, onClose, onSubmit, selectedColumn}) {
const normalizedFilter = React.useMemo(() => normalizeGridColumnFilter(filterValue), [filterValue]);
const [includeText, setIncludeText] = React.useState(normalizedFilter.include.join("\n"));
const [excludeText, setExcludeText] = React.useState(normalizedFilter.exclude.join("\n"));
const [includeMode, setIncludeMode] = React.useState(normalizedFilter.includeMode);
const [caseSensitive, setCaseSensitive] = React.useState(normalizedFilter.caseSensitive);
const columnName = selectedColumn?.name || selectedColumn?.plaintext || selectedColumn?.key || "Column";
const buildFilter = () => ({
include: splitFilterTerms(includeText),
exclude: splitFilterTerms(excludeText),
includeMode,
caseSensitive,
});
const handleSubmit = () => {
onSubmit(buildFilter());
onClose();
};
const handleClear = () => {
onSubmit({...DEFAULT_FILTER});
onClose();
};
const onIncludeModeChange = (event, value) => {
if(value !== null){
setIncludeMode(value);
}
};
return (
<div className="mythic-grid-filter-dialog bg-surface-raised text-primary">
<DialogTitle className="mythic-grid-filter-dialog-title bg-header text-header text-base font-850 leading-120 border-b-subtle">
Filter {columnName}
</DialogTitle>
<DialogContent className="mythic-grid-filter-dialog-content flex flex-column">
<Typography className="mythic-grid-filter-dialog-copy text-muted text-xs font-650 leading-135" component="div">
Add one term per line, or separate terms with commas. Include terms decide what can stay visible; exclude terms remove matching rows.
</Typography>
<Box className="mythic-grid-filter-dialog-mode-row items-center flex gap-6 justify-between">
<Typography className="mythic-grid-filter-dialog-label text-xs font-800 text-primary" component="div">
Include matching
</Typography>
<ToggleButtonGroup
exclusive
onChange={onIncludeModeChange}
size="small"
value={includeMode}
>
<ToggleButton value="any">Any</ToggleButton>
<ToggleButton value="all">All</ToggleButton>
</ToggleButtonGroup>
</Box>
<div className="mythic-grid-filter-dialog-fields grid grid-cols-2">
<TextField
fullWidth
label="Include rows matching"
minRows={4}
multiline
autoFocus
onChange={(event) => setIncludeText(event.target.value)}
placeholder={"admin\nprod"}
value={includeText}
/>
<TextField
fullWidth
label="Exclude rows matching"
minRows={4}
multiline
onChange={(event) => setExcludeText(event.target.value)}
placeholder={"debug\ntest"}
value={excludeText}
/>
</div>
<FormControlLabel
control={
<Switch
checked={caseSensitive}
onChange={(event) => setCaseSensitive(event.target.checked)}
size="small"
/>
}
label="Case sensitive"
/>
</DialogContent>
<DialogActions className="mythic-grid-filter-dialog-actions py-6 px-8 border-t-subtle">
<MythicActionButton onClick={handleClear}>Clear</MythicActionButton>
<MythicActionButton onClick={onClose}>Cancel</MythicActionButton>
<MythicActionButton colorMode="always" onClick={handleSubmit} tone="primary" variant="contained">Apply Filter</MythicActionButton>
</DialogActions>
</div>
);
}
@@ -3,15 +3,10 @@ import { useCallback } from 'react';
import useSingleAndDoubleClick from '../../utilities/useSingleAndDoubleClick';
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 FilterAltOutlinedIcon from '@mui/icons-material/FilterAltOutlined';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import {ContextMenu} from "./Cell";
const HeaderCell = ({
onClick = () => {},
@@ -20,12 +15,17 @@ const HeaderCell = ({
sortIndicatorIndex,
sortDirection,
headerNameKey = "name",
isResizing = false,
onResizePointerDown = () => {},
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 isSorted = sortIndicatorIndex === columnIndex;
const HeaderSortIcon = sortDirection === 'ASC' ? KeyboardArrowUpIcon : KeyboardArrowDownIcon;
const headerLabel = item?.[headerNameKey] || "";
const contextMenuLocationRef = React.useRef({x: 0, y: 0});
const handleClick = useCallback(
(e) => {
onClick(e, columnIndex);
@@ -39,66 +39,131 @@ const HeaderCell = ({
},
[onDoubleClick, columnIndex]
);
const handleResizePointerDown = useCallback(
(event) => {
event.preventDefault();
event.stopPropagation();
onResizePointerDown(event, columnIndex);
},
[onResizePointerDown, columnIndex]
);
const handleResizeDoubleClick = useCallback(
(event) => {
event.preventDefault();
event.stopPropagation();
onDoubleClick(event, columnIndex);
},
[onDoubleClick, columnIndex]
);
const stopResizeClickPropagation = useCallback(
(event) => {
event.preventDefault();
event.stopPropagation();
},
[]
);
const [openContextMenu, setOpenContextMenu] = React.useState(false);
const menuContext = React.useMemo(() => {
return {columnIndex, rowIndex, data: data.items[rowIndex][columnIndex]?.props?.rowData || {}};
}, [columnIndex, data.items, rowIndex]);
const resolvedContextMenuOptions = React.useMemo(() => {
const resolveOption = (option) => {
const resolvedOption = {
...option,
disabled: typeof option.disabled === "function" ? option.disabled(menuContext) : option.disabled,
};
if(option.menuItems){
resolvedOption.menuItems = option.menuItems.map(resolveOption);
}
return resolvedOption;
};
return contextMenuOptions.map(resolveOption);
}, [contextMenuOptions, menuContext]);
const handleContextClick = useCallback(
(event) => {
event.preventDefault();
if(item.disableFilterMenu){
return;
}
if(contextMenuOptions && contextMenuOptions.length > 0){
if(resolvedContextMenuOptions && resolvedContextMenuOptions.length > 0){
contextMenuLocationRef.current.x = event.clientX;
contextMenuLocationRef.current.y = event.clientY;
setOpenContextMenu(true);
}
},
[contextMenuOptions, columnIndex] // eslint-disable-line react-hooks/exhaustive-deps
[resolvedContextMenuOptions]
);
const handleMenuItemClick = (event, index) => {
contextMenuOptions[index].click({event, columnIndex});
const handleMenuItemClick = (event, clickOption) => {
event.preventDefault();
event.stopPropagation();
clickOption({event, ...menuContext});
setOpenContextMenu(false);
};
const handleClose = (event) => {
if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
return;
}
setOpenContextMenu(false);
};
const filterMenuOption = React.useMemo(
() => resolvedContextMenuOptions.find((option) => option.type === "item" && option.name === "Filter Column"),
[resolvedContextMenuOptions]
);
const handleFilterIndicatorClick = useCallback(
(event) => {
event.preventDefault();
event.stopPropagation();
if(filterMenuOption?.disabled || !filterMenuOption?.click){
return;
}
filterMenuOption.click({event, ...menuContext});
setOpenContextMenu(false);
},
[filterMenuOption, menuContext]
);
const handleClicks = useSingleAndDoubleClick(handleClick, handleDoubleClick);
const className = `${classes.headerCell} ${item.disableSort ? classes.headerCellNoSort : ""}`;
return (
<div style={style} className={classes.headerCell} onClick={handleClicks} onContextMenu={handleContextClick} ref={dropdownAnchorRef}>
<Box display='flex' alignItems='center' justifyContent='space-between' width='100%'>
<Typography className={classes.cellInner} variant='body1'>
{item[headerNameKey].toUpperCase()}
<div
style={style}
className={className}
onClick={handleClicks}
onContextMenu={handleContextClick}
ref={dropdownAnchorRef}
title={typeof headerLabel === "string" ? headerLabel : undefined}
>
<Box className={classes.headerContent}>
<Typography className={`${classes.cellInner} ${classes.headerLabel}`} component='div' variant='body1'>
{typeof headerLabel === "string" ? headerLabel : headerLabel}
</Typography>
{isFiltered && <FontAwesomeIcon icon={faFilter} />}
{sortIndicatorIndex === columnIndex && (sortDirection === 'ASC' ? <div>↑</div> : <div>↓</div>)}
<Popper open={openContextMenu} anchorEl={dropdownAnchorRef.current} role={undefined} transition disablePortal style={{zIndex: 4}}>
{({ 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, color: "white"}}>
<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>
<Box className={classes.headerActions}>
{isFiltered &&
<button
aria-label={`Edit filter for ${typeof headerLabel === "string" ? headerLabel : "column"}`}
className={`${classes.headerIndicator} ${classes.headerFilterIcon}`}
onClick={handleFilterIndicatorClick}
title="Edit Filter"
type="button"
>
<FilterAltOutlinedIcon fontSize="inherit" />
</button>
}
{isSorted &&
<span className={`${classes.headerIndicator} ${classes.headerSortIcon}`} title={`Sorted ${sortDirection === 'ASC' ? 'ascending' : 'descending'}`}>
<HeaderSortIcon fontSize="inherit" />
</span>
}
</Box>
<ContextMenu dropdownAnchorRef={dropdownAnchorRef} contextMenuOptions={resolvedContextMenuOptions}
disableFilterMenu={item?.disableFilterMenu} openContextMenu={openContextMenu}
contextMenuLocationRef={contextMenuLocationRef}
setOpenContextMenu={setOpenContextMenu} handleMenuItemClick={handleMenuItemClick}
/>
</Box>
{!item.disableResize &&
<div
role="separator"
aria-orientation="vertical"
aria-label={`Resize ${typeof headerLabel === "string" ? headerLabel : "column"} column`}
className={`${classes.headerResizeHandle} ${isResizing ? classes.headerResizeHandleActive : ""}`}
onPointerDown={handleResizePointerDown}
onDoubleClick={handleResizeDoubleClick}
onClick={stopResizeClickPropagation}
/>
}
</div>
);
};
@@ -5,19 +5,218 @@ import useScrollbarSize from 'react-scrollbar-size';
import { VariableSizeGrid } from 'react-window';
import HeaderCell from './HeaderCell';
import Cell from './Cell';
import DraggableHandles from './DraggableHandles';
import {classes} from './styles';
import {GetMythicSetting, useSetMythicSetting} from "../MythicSavedUserSetting";
import FitScreenIcon from '@mui/icons-material/FitScreen';
import RestartAltIcon from '@mui/icons-material/RestartAlt';
import ViewColumnIcon from '@mui/icons-material/ViewColumn';
import FilterAltOutlinedIcon from '@mui/icons-material/FilterAltOutlined';
const HeaderCellContext = createContext({});
const MIN_COLUMN_WIDTH = 100;
const CellRenderer = (VariableSizeGridProps) => {
return VariableSizeGridProps.rowIndex === 0 ? null : <Cell VariableSizeGridProps={VariableSizeGridProps} />;
const MIN_COLUMN_WIDTH = 50;
const MIN_FLEX_COLUMN_WIDTH = 50;
const AUTOSIZE_HORIZONTAL_PADDING = 40;
const AUTOSIZE_HEADER_EXTRA_WIDTH = 30;
const headerMenuIconStyle = {fontSize: "1rem", marginRight: "8px"};
const normalizeHeaderContextMenuOption = (option) => {
if(option?.name === "Filter Column"){
return {
...option,
icon: <FilterAltOutlinedIcon style={headerMenuIconStyle} />,
};
}
if(option?.menuItems){
return {
...option,
menuItems: option.menuItems.map(normalizeHeaderContextMenuOption),
};
}
return option;
};
const innerElementType = React.forwardRef(({ children, ...rest }, ref) => {
let autosizeCanvas;
const getNumericWidth = (value) => {
const width = Number(value);
return Number.isFinite(width) ? width : undefined;
};
const getColumnMinWidth = (column) => {
const configuredMinWidth = getNumericWidth(column?.minWidth);
return Math.max(configuredMinWidth || 0, column?.fillWidth ? MIN_FLEX_COLUMN_WIDTH : MIN_COLUMN_WIDTH);
};
const getColumnMaxWidth = (column) => {
const configuredMaxWidth = getNumericWidth(column?.maxWidth);
return configuredMaxWidth && configuredMaxWidth > 0 ? configuredMaxWidth : Infinity;
};
const clampColumnWidth = (column, width) => {
const minWidth = getColumnMinWidth(column);
const maxWidth = getColumnMaxWidth(column);
const numericWidth = getNumericWidth(width) || minWidth;
return Math.floor(Math.min(maxWidth, Math.max(numericWidth, minWidth)));
};
const getPreferredColumnWidth = (column) => {
return column?.width === undefined ? getColumnMinWidth(column) : column.width;
};
const distributeFillWidth = ({columns, columnWidths, totalWidth}) => {
const availableWidth = Math.max(0, Math.floor(totalWidth || 0));
const currentWidth = columnWidths.reduce((a, b) => a + b, 0);
const widthDifference = availableWidth - currentWidth;
if(widthDifference <= 0 || columns.length === 0){
return columnWidths;
}
const fillColumnIndexes = columns.reduce((previous, column, index) => {
return column.fillWidth ? [...previous, index] : previous;
}, []);
const targetIndexes = fillColumnIndexes.length > 0 ? fillColumnIndexes : [columns.length - 1];
let remainingWidth = widthDifference;
const updatedColumnWidths = [...columnWidths];
for(let i = 0; i < targetIndexes.length; i++){
const columnIndex = targetIndexes[i];
const share = i === targetIndexes.length - 1 ? remainingWidth : Math.floor(widthDifference / targetIndexes.length);
const currentColumnWidth = updatedColumnWidths[columnIndex];
const nextColumnWidth = clampColumnWidth(columns[columnIndex], currentColumnWidth + share);
updatedColumnWidths[columnIndex] = nextColumnWidth;
remainingWidth -= nextColumnWidth - currentColumnWidth;
}
return updatedColumnWidths;
};
const getInitialColumnWidths = ({columns, savedWidths = [], totalWidth = 0}) => {
const hasSavedWidths = savedWidths.length === columns.length;
const baseColumnWidths = columns.map((column, index) => {
return clampColumnWidth(column, hasSavedWidths ? savedWidths[index] : getPreferredColumnWidth(column));
});
if(hasSavedWidths){
return baseColumnWidths;
}
return distributeFillWidth({columns, columnWidths: baseColumnWidths, totalWidth});
};
const areColumnWidthsEqual = (first = [], second = []) => {
if(first.length !== second.length){
return false;
}
return first.every((width, index) => Math.floor(width || MIN_COLUMN_WIDTH) === Math.floor(second[index] || MIN_COLUMN_WIDTH));
};
const normalizeColumnWidths = (columnWidths = []) => {
return columnWidths.map((columnWidth) => Math.floor(getNumericWidth(columnWidth) || MIN_COLUMN_WIDTH));
};
const getMeasureContext = () => {
if(typeof document === "undefined"){
return null;
}
if(autosizeCanvas === undefined){
autosizeCanvas = document.createElement("canvas");
}
const context = autosizeCanvas.getContext("2d");
if(context === null){
return null;
}
const referenceElement = document.querySelector(`.${classes.cell}`) || document.body;
const computedStyle = window.getComputedStyle(referenceElement);
context.font = [
computedStyle.fontStyle,
computedStyle.fontVariant,
computedStyle.fontWeight,
computedStyle.fontSize,
computedStyle.fontFamily,
].join(" ");
return context;
};
const getDisplayText = (value) => {
if(value === undefined || value === null){
return "";
}
if(typeof value === "string"){
const trimmedValue = value.trim();
if(trimmedValue.startsWith("[")){
try{
const parsedValue = JSON.parse(trimmedValue);
if(Array.isArray(parsedValue) && parsedValue.length > 0){
return getDisplayText(parsedValue[0]);
}
}catch(error){
// Keep the original string when it is not a serialized array.
}
}
return value;
}
if(typeof value === "number" || typeof value === "boolean"){
return String(value);
}
if(Array.isArray(value)){
return value.map((entry) => getDisplayText(entry)).filter(Boolean).join(", ");
}
if(React.isValidElement(value)){
return getDisplayText(value.props?.cellData || value.props?.children);
}
if(typeof value === "object"){
if(value.plaintext !== undefined){
return getDisplayText(value.plaintext);
}
if(value.name !== undefined){
return getDisplayText(value.name);
}
if(value.button?.name !== undefined){
return getDisplayText(value.button.name);
}
if(value.display !== undefined){
return getDisplayText(value.display);
}
return "";
}
return "";
};
const getCellAutosizeText = ({column, cell}) => {
const cellProps = cell?.props || {};
const columnKey = column?.key || column?.plaintext;
const rowData = cellProps.rowData || {};
const cellDataText = getDisplayText(cellProps.cellData);
if(cellDataText !== ""){
return cellDataText;
}
if(columnKey !== undefined){
return getDisplayText(rowData?.[columnKey]);
}
return getDisplayText(cell);
};
const measureAutosizeText = (context, text) => {
const normalizedText = getDisplayText(text);
if(normalizedText === ""){
return 0;
}
const lines = normalizedText.split(/\r?\n/);
return lines.reduce((longestLineWidth, line) => {
const textWidth = context ? context.measureText(line).width : line.length * 8;
return Math.max(longestLineWidth, textWidth);
}, 0);
};
const getAutosizedColumnWidth = ({column, columnIndex, headerNameKey, items}) => {
const measureContext = getMeasureContext();
const headerText = getDisplayText(column?.[headerNameKey] || column?.name || column?.plaintext);
const headerWidth = measureAutosizeText(measureContext, headerText.toUpperCase()) + AUTOSIZE_HEADER_EXTRA_WIDTH;
const widestCellWidth = items.reduce((widestWidth, itemRow) => {
const cellText = getCellAutosizeText({column, cell: itemRow[columnIndex]});
return Math.max(widestWidth, measureAutosizeText(measureContext, cellText));
}, 0);
return clampColumnWidth(column, Math.max(headerWidth, widestCellWidth) + AUTOSIZE_HORIZONTAL_PADDING);
};
const CellRendererPreMemo = ({ style, rowIndex, columnIndex, data }) => {
return rowIndex === 0 ? null : <Cell style={style} rowIndex={rowIndex} columnIndex={columnIndex} data={data} />;
};
const CellRenderer = React.memo(CellRendererPreMemo);
const innerElementType = React.forwardRef(({ children, style }, ref) => {
const HeaderCellData = useContext(HeaderCellContext);
const resizingColumnIndex = HeaderCellData.resizingColumnIndex;
const resizeGuideLeft = resizingColumnIndex >= 0 ?
HeaderCellData.columnWidths.slice(0, resizingColumnIndex + 1).reduce((a, b) => a + b, 0) :
-1;
const onHeaderDoubleClick = React.useCallback( (e, columnIndex) => {
if (HeaderCellData.columns[columnIndex].disableAutosize) return;
HeaderCellData.autosizeColumn({columnIndex});
}, [HeaderCellData.columns, HeaderCellData.autosizeColumn]);
return (
<div ref={ref} {...rest}>
<div ref={ref} style={{...style, position: "relative"}}>
{/* always render header cells */}
<div
className={classes.headerCellRow}
@@ -31,13 +230,12 @@ const innerElementType = React.forwardRef(({ children, ...rest }, ref) => {
key={i}
headerNameKey={HeaderCellData.headerNameKey}
onClick={HeaderCellData.onClickHeader}
onDoubleClick={(e, columnIndex) => {
if (column.disableAutosize) return;
HeaderCellData.autosizeColumn({columnIndex});
}}
onDoubleClick={onHeaderDoubleClick}
contextMenuOptions={HeaderCellData.contextMenuOptions}
sortIndicatorIndex={HeaderCellData.sortIndicatorIndex}
sortDirection={HeaderCellData.sortDirection}
isResizing={HeaderCellData.resizingColumnIndex === i}
onResizePointerDown={HeaderCellData.startColumnResize}
VariableSizeGridProps={{
style: {
position: 'absolute',
@@ -56,14 +254,24 @@ const innerElementType = React.forwardRef(({ children, ...rest }, ref) => {
</div>
{/* render other cells as usual */}
{children}
{resizingColumnIndex >= 0 &&
<div
className={classes.resizeGuide}
style={{
left: resizeGuideLeft,
height: style.height,
}}
/>
}
</div>
);
});
const getShortRandomString = () => {
export const GetShortRandomString = () => {
return (Math.random() + 1).toString(36).substring(2);
}
const ResizableGridWrapper = ({
name,
columns,
sortIndicatorIndex,
sortDirection,
@@ -73,17 +281,43 @@ const ResizableGridWrapper = ({
onDoubleClickRow,
contextMenuOptions,
rowContextMenuOptions,
onRowContextMenuClick,
rowHeight,
headerRowHeight,
widthMeasureKey,
callbackTableGridRef,
onRowClick,
...AutoSizerProps
}) => {
/* Hooks */
const { width: scrollbarWidth } = useScrollbarSize();
const [columnWidths, setColumnWidths] = useState(columns.map((column) => column.width || MIN_COLUMN_WIDTH));
const gridUUID = React.useMemo( () => getShortRandomString(), []);
const localColumnsRef = React.useRef(columns);
const initialSavedWidths = name === undefined ? [] : GetMythicSetting({setting_name: `${name}_column_widths`, default_value: {}, output: "json-array"});
const savedColumnWidths = Array.isArray(initialSavedWidths) ? initialSavedWidths : [];
const [columnWidths, setColumnWidths] = useState(() => getInitialColumnWidths({
columns,
savedWidths: savedColumnWidths,
totalWidth: AutoSizerProps.width - scrollbarWidth,
}));
const gridUUID = React.useMemo( () => GetShortRandomString(), []);
const gridRef = useRef(null);
const dragHandlesRef = useRef(null);
const columnWidthsRef = useRef(columnWidths);
const lastResetColumnIndexRef = useRef(0);
const activeResizeRef = useRef(null);
const resizeFrameRef = useRef(null);
const cleanupResizeListenersRef = useRef(null);
const [resizingColumnIndex, setResizingColumnIndex] = useState(-1);
const [updateSetting] = useSetMythicSetting();
const setColumnWidthsAndRef = useCallback( (newColumnWidths, resetColumnIndex=0) => {
const normalizedColumnWidths = normalizeColumnWidths(newColumnWidths);
if(areColumnWidthsEqual(columnWidthsRef.current, normalizedColumnWidths)){
return false;
}
lastResetColumnIndexRef.current = resetColumnIndex;
columnWidthsRef.current = normalizedColumnWidths;
setColumnWidths(normalizedColumnWidths);
return true;
}, []);
const getColumnWidth = useCallback(
(index) => {
return columnWidths[index] || MIN_COLUMN_WIDTH;
@@ -92,111 +326,210 @@ const ResizableGridWrapper = ({
);
const getRowHeight = useCallback(
(index) => {
if(index === 0){
return headerRowHeight;
}
return rowHeight;
},
[rowHeight]
[rowHeight, headerRowHeight]
);
useEffect(() => {
const totalWidth = AutoSizerProps.width - scrollbarWidth;
const updatedColumnWidths = columns.map((column) => column.width || MIN_COLUMN_WIDTH);
const totalWidthDiff = totalWidth - updatedColumnWidths.reduce((a, b) => a + b, 0);
if (totalWidthDiff > 0) {
let updatedWidthIndexs = [];
for(let i = 0; i < columns.length; i++){
// check if any of the columns have the `fillWidth` property to true
if(columns[i]["fillWidth"]){
updatedWidthIndexs.push(i);
}
}
if(updatedWidthIndexs.length === 0){
updatedWidthIndexs.push(columns.length - 1);
}
for(let i = 0; i < updatedWidthIndexs.length; i++){
updatedColumnWidths[updatedWidthIndexs[i]] += totalWidthDiff / updatedWidthIndexs.length;
}
//updatedColumnWidths[updatedWidthIndex] += totalWidthDiff;
if(savedColumnWidths.length === columns.length && localColumnsRef.current.length === columns.length){
return;
}
setColumnWidths(updatedColumnWidths);
}, [scrollbarWidth, columns, AutoSizerProps.width]); // eslint-disable-line react-hooks/exhaustive-deps
localColumnsRef.current = columns;
const updatedColumnWidths = getInitialColumnWidths({
columns,
savedWidths: [],
totalWidth: AutoSizerProps.width - scrollbarWidth,
});
const didUpdateWidths = setColumnWidthsAndRef(updatedColumnWidths);
if(didUpdateWidths && name !== undefined){
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths, broadcast: false});
}
}, [scrollbarWidth, columns, AutoSizerProps.width, localColumnsRef.current, setColumnWidthsAndRef]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
gridRef.current.resetAfterColumnIndex(0, true);
if(gridRef.current){
gridRef.current.resetAfterColumnIndex(lastResetColumnIndexRef.current, true);
}
}, [columnWidths]);
/* Event Handlers */
const resizeColumn = (x, columnIndex) => {
const updatedWidths = columnWidths.map((columnWidth, index) => {
const startColumnResize = useCallback( (event, columnIndex) => {
if(columns[columnIndex]?.disableResize){
return;
}
const initialWidths = columnWidthsRef.current.map( (columnWidth) => Math.floor(columnWidth || MIN_COLUMN_WIDTH));
const startClientX = event.clientX;
const startWidth = initialWidths[columnIndex] || MIN_COLUMN_WIDTH;
const previousCursor = document.body.style.cursor;
const previousUserSelect = document.body.style.userSelect;
const applyResizeWidth = (clientX) => {
const activeResize = activeResizeRef.current;
if(!activeResize){
return;
}
activeResize.latestClientX = clientX;
if(resizeFrameRef.current !== null){
return;
}
resizeFrameRef.current = window.requestAnimationFrame(() => {
resizeFrameRef.current = null;
const currentResize = activeResizeRef.current;
if(!currentResize){
return;
}
const resizeDelta = currentResize.latestClientX - currentResize.startClientX;
const nextColumnWidth = clampColumnWidth(columns[currentResize.columnIndex], currentResize.startWidth + resizeDelta);
const updatedColumnWidths = currentResize.initialWidths.map( (columnWidth, index) => {
return currentResize.columnIndex === index ? nextColumnWidth : columnWidth;
});
setColumnWidthsAndRef(updatedColumnWidths, currentResize.columnIndex);
});
};
const cleanupResizeListeners = () => {
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerUp);
window.removeEventListener("pointercancel", handlePointerUp);
document.body.style.cursor = previousCursor;
document.body.style.userSelect = previousUserSelect;
cleanupResizeListenersRef.current = null;
};
const finishResize = (clientX) => {
const currentResize = activeResizeRef.current;
if(!currentResize){
cleanupResizeListeners();
return;
}
if(resizeFrameRef.current !== null){
window.cancelAnimationFrame(resizeFrameRef.current);
resizeFrameRef.current = null;
}
const resizeDelta = clientX - currentResize.startClientX;
const nextColumnWidth = clampColumnWidth(columns[currentResize.columnIndex], currentResize.startWidth + resizeDelta);
const updatedColumnWidths = currentResize.initialWidths.map( (columnWidth, index) => {
return currentResize.columnIndex === index ? nextColumnWidth : columnWidth;
});
activeResizeRef.current = null;
const didUpdateWidths = setColumnWidthsAndRef(updatedColumnWidths, currentResize.columnIndex);
if(didUpdateWidths && name !== undefined){
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths, broadcast: false});
}
setResizingColumnIndex(-1);
cleanupResizeListeners();
};
function handlePointerMove(pointerEvent){
pointerEvent.preventDefault();
applyResizeWidth(pointerEvent.clientX);
}
function handlePointerUp(pointerEvent){
pointerEvent.preventDefault();
finishResize(pointerEvent.clientX);
}
event.currentTarget.setPointerCapture?.(event.pointerId);
activeResizeRef.current = {
columnIndex,
initialWidths,
latestClientX: startClientX,
startClientX,
startWidth,
};
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
setResizingColumnIndex(columnIndex);
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", handlePointerUp);
window.addEventListener("pointercancel", handlePointerUp);
cleanupResizeListenersRef.current = cleanupResizeListeners;
}, [columns, name, setColumnWidthsAndRef, updateSetting]);
useEffect(() => {
return () => {
cleanupResizeListenersRef.current?.();
activeResizeRef.current = null;
if(resizeFrameRef.current !== null){
window.cancelAnimationFrame(resizeFrameRef.current);
resizeFrameRef.current = null;
}
};
}, []);
const autosizeColumn = useCallback( ({columnIndex}) => {
const column = columns[columnIndex];
if(column?.disableDoubleClick || column?.disableAutosize){
return
}
const currentColumnWidths = columnWidthsRef.current;
const updatedColumnWidths = currentColumnWidths.map((columnWidth, index) => {
if (columnIndex === index) {
return Math.floor(Math.max(columnWidth + x, MIN_COLUMN_WIDTH));
return getAutosizedColumnWidth({column, columnIndex, headerNameKey, items});
}
return Math.floor(columnWidth);
});
setColumnWidths(updatedWidths);
};
const autosizeColumn = ({columnIndex}) => {
const longestElementInColumn = Math.max(...items.map((itemRow) => {
if(!columns[columnIndex].key){
if(columns[columnIndex].plaintext){
columns[columnIndex].key = columns[columnIndex].plaintext;
} else {
return 30;
}
const didUpdateWidths = setColumnWidthsAndRef(updatedColumnWidths, columnIndex);
if(didUpdateWidths){
if(name !== undefined){
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths, broadcast: false});
}
if(columns[columnIndex].key){
if(columns[columnIndex].key.includes("time")){
return 30;
}
if(columns[columnIndex].key === "mythictree_groups"){
return itemRow[columnIndex]?.props?.cellData.length;
}
try{
items = JSON.parse(itemRow[columnIndex]?.props?.rowData?.[columns[columnIndex].key]);
if(Array.isArray(items) && items.length > 0){
return String(items[0]).length;
}
}catch(error){
//console.log(itemRow[columnIndex]?.props?.rowData?.[columns[columnIndex].key])
}
let data = itemRow[columnIndex]?.props?.rowData?.[columns[columnIndex].key];
if(!data){
return 3;
}
if(data.plaintext){
return String(data.plaintext).length || -1;
} else {
return String(data).length || -1;
}
//return String(itemRow[columnIndex]?.props?.rowData?.[columns[columnIndex].key]).length || -1;
} else if(typeof(itemRow[columnIndex]?.props?.cellData) === "string") {
try {
items = JSON.parse(itemRow[columnIndex]?.props?.cellData);
if (Array.isArray(items) && items.length > 0) {
return String(items[0]).length;
}
} catch (error) {
return itemRow[columnIndex]?.props?.cellData.length;
}
} else if(Array.isArray(itemRow[columnIndex]?.props?.cellData)){
return itemRow[columnIndex]?.props?.cellData.join(", ").length;
}else {
return itemRow[columnIndex]?.props?.cellData?.length || -1;
}
}, [columns, headerNameKey, items, name, setColumnWidthsAndRef, updateSetting]);
const autosizeAllColumns = useCallback( () => {
const currentColumnWidths = columnWidthsRef.current;
const updatedColumnWidths = currentColumnWidths.map((columnWidth, columnIndex) => {
const column = columns[columnIndex];
if(column?.disableDoubleClick || column?.disableAutosize){
return Math.floor(columnWidth);
}
}));
const updatedWidths = columnWidths.map((columnWidth, index) => {
if (columnIndex === index) {
return Math.floor(Math.max(longestElementInColumn * 10 + 40, MIN_COLUMN_WIDTH));
}
return Math.floor(columnWidth);
return getAutosizedColumnWidth({column, columnIndex, headerNameKey, items});
});
setColumnWidths(updatedWidths);
};
const didUpdateWidths = setColumnWidthsAndRef(updatedColumnWidths, 0);
if(didUpdateWidths && name !== undefined){
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths, broadcast: false});
}
}, [columns, headerNameKey, items, name, setColumnWidthsAndRef, updateSetting]);
const resetColumnWidths = useCallback( () => {
const updatedColumnWidths = getInitialColumnWidths({
columns,
savedWidths: [],
totalWidth: AutoSizerProps.width - scrollbarWidth,
});
const didUpdateWidths = setColumnWidthsAndRef(updatedColumnWidths, 0);
if(didUpdateWidths && name !== undefined){
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths, broadcast: false});
}
}, [AutoSizerProps.width, columns, name, scrollbarWidth, setColumnWidthsAndRef, updateSetting]);
const headerContextMenuOptions = React.useMemo( () => {
const sizingOptions = [
{
name: "Autosize Column",
type: "item",
icon: <FitScreenIcon style={headerMenuIconStyle} />,
disabled: ({columnIndex}) => columns[columnIndex]?.disableDoubleClick || columns[columnIndex]?.disableAutosize,
click: ({columnIndex}) => autosizeColumn({columnIndex}),
},
{
name: "Autosize All Columns",
type: "item",
icon: <ViewColumnIcon style={headerMenuIconStyle} />,
disabled: columns.every((column) => column?.disableDoubleClick || column?.disableAutosize),
click: () => autosizeAllColumns(),
},
{
name: "Reset Column Widths",
type: "item",
icon: <RestartAltIcon style={headerMenuIconStyle} />,
click: () => resetColumnWidths(),
},
];
return [...sizingOptions, ...(contextMenuOptions || []).map(normalizeHeaderContextMenuOption)];
}, [autosizeAllColumns, autosizeColumn, columns, contextMenuOptions, resetColumnWidths]);
useEffect( () => {
if(callbackTableGridRef){
callbackTableGridRef.current = gridRef.current;
}
}, [gridRef.current])
const itemsWithHeader = [columns, ...items];
const headerCellData = {
"getRowHeight": getRowHeight,
@@ -205,16 +538,19 @@ const ResizableGridWrapper = ({
"headerNameKey": headerNameKey,
"onClickHeader": onClickHeader,
"autosizeColumn": autosizeColumn,
"contextMenuOptions": contextMenuOptions,
"contextMenuOptions": headerContextMenuOptions,
"sortIndicatorIndex": sortIndicatorIndex,
"sortDirection": sortDirection,
"getColumnWidth": getColumnWidth,
"resizingColumnIndex": resizingColumnIndex,
"startColumnResize": startColumnResize,
"itemsWithHeader": itemsWithHeader,
}
};
return (
<>
<HeaderCellContext.Provider value={headerCellData}>
<VariableSizeGrid
className={classes.grid}
height={Math.max(1, AutoSizerProps.height)}
width={AutoSizerProps.width}
columnCount={columns.length}
@@ -222,26 +558,16 @@ const ResizableGridWrapper = ({
headerNameKey={headerNameKey}
rowCount={itemsWithHeader.length}
rowHeight={getRowHeight}
itemData={{ items: itemsWithHeader, onDoubleClickRow, gridUUID, rowContextMenuOptions}}
itemData={{ items: itemsWithHeader,
onDoubleClickRow, gridUUID,
rowContextMenuOptions, onRowClick,
onRowContextMenuClick}}
innerElementType={innerElementType}
overscanRowCount={5}
onScroll={({ scrollLeft }) => {
if (dragHandlesRef.current) {
dragHandlesRef.current.scrollTo({ left: scrollLeft });
}
}}
overscanRowCount={20}
useIsScrolling={false}
ref={gridRef}>
{CellRenderer}
</VariableSizeGrid>
<DraggableHandles
height={AutoSizerProps.height}
rowHeight={getRowHeight(0)}
width={AutoSizerProps.width}
minColumnWidth={MIN_COLUMN_WIDTH}
columnWidths={columnWidths}
onStop={resizeColumn}
ref={dragHandlesRef}
/>
</HeaderCellContext.Provider>
</>
@@ -258,28 +584,40 @@ const MythicResizableGrid = ({
onDoubleClickRow,
contextMenuOptions,
rowContextMenuOptions,
onRowContextMenuClick,
widthMeasureKey,
rowHeight = 32,
rowHeight = 20,
headerRowHeight = 20,
callbackTableGridRef,
onRowClick,
name,
}) => {
return (
<AutoSizer style={{height: "100%"}}>
{(AutoSizerProps) => (
<ResizableGridWrapper
columns={columns}
headerNameKey={headerNameKey}
sortIndicatorIndex={sortIndicatorIndex}
sortDirection={sortDirection}
items={items}
widthMeasureKey={widthMeasureKey}
rowHeight={rowHeight}
onClickHeader={onClickHeader}
onDoubleClickRow={onDoubleClickRow}
contextMenuOptions={contextMenuOptions}
rowContextMenuOptions={rowContextMenuOptions}
{...AutoSizerProps}
/>
)}
</AutoSizer>
<div className={classes.root}>
<AutoSizer style={{height: "100%", width: "100%"}}>
{(AutoSizerProps) => (
<ResizableGridWrapper
name={name}
columns={columns}
callbackTableGridRef={callbackTableGridRef}
headerNameKey={headerNameKey}
sortIndicatorIndex={sortIndicatorIndex}
sortDirection={sortDirection}
items={items}
widthMeasureKey={widthMeasureKey}
rowHeight={rowHeight}
headerRowHeight={headerRowHeight}
onClickHeader={onClickHeader}
onDoubleClickRow={onDoubleClickRow}
contextMenuOptions={contextMenuOptions}
rowContextMenuOptions={rowContextMenuOptions}
onRowContextMenuClick={onRowContextMenuClick}
onRowClick={onRowClick}
{...AutoSizerProps}
/>
)}
</AutoSizer>
</div>
);
};
@@ -289,6 +627,7 @@ MythicResizableGrid.propTypes = {
name: PropTypes.string,
width: PropTypes.number,
disableAutosize: PropTypes.bool,
disableResize: PropTypes.bool,
disableSort: PropTypes.bool
})
).isRequired,
@@ -300,6 +639,7 @@ MythicResizableGrid.propTypes = {
contextMenuOptions: PropTypes.array,
rowContextMenuOptions: PropTypes.array,
rowHeight: PropTypes.number,
headerRowHeight: PropTypes.number,
headerNameKey: PropTypes.string,
widthMeasureKey: PropTypes.string
};
@@ -1,11 +1,24 @@
const PREFIX = 'MythicResizableGrid';
export const classes = {
headerCellRow: `${PREFIX}-headerCellRow`,
headerCell: `${PREFIX}-headerCell`,
root: `${PREFIX}-root flex-fill h-full min-h-0 min-w-0 overflow-hidden relative rounded w-full`,
grid: `${PREFIX}-grid`,
headerCellRow: `${PREFIX}-headerCellRow flex flex-row`,
headerCell: `${PREFIX}-headerCell cursor-pointer items-center flex justify-between min-w-0 overflow-hidden relative`,
headerCellNoSort: `${PREFIX}-headerCellNoSort`,
headerContent: `${PREFIX}-headerContent items-center flex gap-3 justify-between min-w-0 w-full`,
headerLabel: `${PREFIX}-headerLabel min-w-0 truncate`,
headerActions: `${PREFIX}-headerActions items-center inline-flex flex-none gap-2 min-w-0`,
headerIndicator: `${PREFIX}-headerIndicator items-center inline-flex flex-none justify-center rounded`,
headerFilterIcon: `${PREFIX}-headerFilterIcon cursor-pointer`,
headerSortIcon: `${PREFIX}-headerSortIcon`,
headerResizeHandle: `${PREFIX}-headerResizeHandle`,
headerResizeHandleActive: `${PREFIX}-headerResizeHandleActive`,
resizeGuide: `${PREFIX}-resizeGuide`,
hoveredRow: `${PREFIX}-hoveredRow`,
cell: `${PREFIX}-cell`,
cellInner: `${PREFIX}-cellInner`,
draggableHandlesContainer: `${PREFIX}-draggableHandlesContainer`,
draggableHandlesClickArea: `${PREFIX}-draggableHandlesClickArea`,
draggableHandlesIndicator: `${PREFIX}-draggableHandlesIndicator`,
contextRow: `${PREFIX}-contextRow`,
rowFirstCell: `${PREFIX}-rowFirstCell`,
rowLastCell: `${PREFIX}-rowLastCell`,
rowInteractive: `${PREFIX}-rowInteractive`,
cell: `${PREFIX}-cell items-center flex min-w-0 relative truncate`,
cellInner: `${PREFIX}-cellInner items-center flex min-w-0 truncate w-full whitespace-nowrap`,
}
@@ -1,50 +1,105 @@
import { useReactiveVar } from '@apollo/client';
import { meState } from '../../cache';
import {mePreferences, operatorSettingDefaults} from '../../cache';
import React from 'react';
import {useMutation, gql } from '@apollo/client';
import {snackActions} from "../utilities/Snackbar";
const updatePreferences = gql`
mutation updatePreferences($preferences: jsonb!){
updateOperatorPreferences(preferences: $preferences){
status
error
}
}
`;
export const GetComputedFontSize = () => {
const element = document.getElementById('root');
const fontSizeString = window.getComputedStyle(element).fontSize;
return parseFloat(fontSizeString);
}
let latestPreferenceSnapshot = null;
const getPreferenceSnapshot = () => {
return latestPreferenceSnapshot || mePreferences() || {};
};
const areSettingValuesEqual = (first, second) => {
if(first === second){
return true;
}
if(typeof first !== "object" || typeof second !== "object" || first === null || second === null){
return false;
}
try{
return JSON.stringify(first) === JSON.stringify(second);
}catch(error){
return false;
}
};
/*
setting_name options:
hideUsernames
showIP
showHostname
showCallbackGroups
taskingDisplayFields
showMedia
*/
export function useMythicSetting({setting_name, default_value, output="boolean"}){
const me = useReactiveVar(meState);
// get the initial value we have stored
const localStorageSetting = localStorage.getItem(`${me?.user?.user_id || 0}-${setting_name}`);
let initialStorageSetting = localStorageSetting === null ? default_value : localStorageSetting;
interactType
switch(output){
case "boolean":
initialStorageSetting = (localStorageSetting.toLowerCase() === "true");
break;
case "number":
initialStorageSetting = Number(localStorageSetting);
break;
default:
console.log("unknown output type", output);
}
callbacks_table_columns
callbacks_table_filters
*/
export function useGetMythicSetting({setting_name, default_value}){
const preferences = useReactiveVar(mePreferences);
let initialStorageSetting = GetMythicSetting({setting_name, default_value});
const [setting, setSetting] = React.useState(initialStorageSetting);
React.useEffect( () => {
// update the initial value if the user changes
const localStorageSetting = localStorage.getItem(`${me?.user?.user_id || 0}-${setting_name}`);
let initialStorageSetting = localStorageSetting === null ? default_value : localStorageSetting;
let newSetting = GetMythicSetting({setting_name, default_value});
setSetting(newSetting);
}, [preferences?.[setting_name]]);
switch(output){
case "boolean":
initialStorageSetting = (localStorageSetting.toLowerCase() === "true");
break;
case "number":
initialStorageSetting = Number(localStorageSetting);
break;
default:
console.log("unknown output type", output);
}
setSetting(initialStorageSetting);
}, [me]);
return setting;
}
}
export function GetMythicSetting({setting_name, default_value}){
const preferences = getPreferenceSnapshot();
return preferences?.[setting_name] === undefined ? default_value : preferences?.[setting_name];
}
export function useSetMythicSetting() {
const [updateSetting] = useMutation(updatePreferences, {
onCompleted: (data) => {
//console.log(data, mePreferences());
},
onError: (error) => {
snackActions.error("failed to save user setting: " + error.message);
console.log(error);
}
});
return [
({setting_name, value, broadcast=true}) => {
const currentPreferences = getPreferenceSnapshot();
if(!areSettingValuesEqual(currentPreferences?.[setting_name], value)){
const updatedPreferences = {
...currentPreferences,
[setting_name]: value,
};
latestPreferenceSnapshot = updatedPreferences;
if(broadcast){
mePreferences(updatedPreferences);
}
updateSetting({variables: {preferences: updatedPreferences}});
}
},
({settings, broadcast=true}) => {
const updatedPreferences = {
...getPreferenceSnapshot(),
...settings,
};
latestPreferenceSnapshot = updatedPreferences;
if(broadcast){
mePreferences(updatedPreferences);
}
updateSetting({variables: {preferences: updatedPreferences}});
},
() => {
latestPreferenceSnapshot = operatorSettingDefaults;
mePreferences(operatorSettingDefaults);
updateSetting({variables: {preferences: operatorSettingDefaults}});
}
]
}
@@ -1,5 +1,5 @@
import React, {useRef, useEffect} from 'react';
import Button from '@mui/material/Button';
import {MythicActionButton} from './MythicActionButton';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
@@ -8,12 +8,9 @@ import FormControl from '@mui/material/FormControl';
import Select from '@mui/material/Select';
import InputLabel from '@mui/material/InputLabel';
import Input from '@mui/material/Input';
import TableCell from '@mui/material/TableCell';
import TableRow from '@mui/material/TableRow';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableContainer from '@mui/material/TableContainer';
import Paper from '@mui/material/Paper';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
export function MythicSelectFromListDialog(props) {
const [options, setOptions] = React.useState([]);
@@ -57,19 +54,19 @@ export function MythicSelectFromListDialog(props) {
<em>None</em>
</MenuItem>
{options.map( (opt) => (
<MenuItem value={opt} key={opt[props.identifier]}>{opt[props.display]}</MenuItem>
<MenuItem value={opt} key={opt[props.identifier]}>{opt?.[props.display]}</MenuItem>
) )}
</Select>
</FormControl>
</React.Fragment>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
<MythicActionButton colorMode="always" onClick={props.onClose} tone="primary" variant="contained">
Close
</Button>
<Button onClick={handleSubmit} variant="contained" color="success">
</MythicActionButton>
<MythicActionButton colorMode="always" onClick={handleSubmit} tone="success" variant="contained">
{props.action}
</Button>
</MythicActionButton>
</DialogActions>
</React.Fragment>
);
@@ -77,10 +74,17 @@ export function MythicSelectFromListDialog(props) {
export function MythicSelectFromRawListDialog(props) {
const [options, setOptions] = React.useState([]);
const actionText = props.action || "Select";
const handleSubmit = (selected) => {
props.onSubmit(selected);
props.onClose();
}
const handleKeyDown = (event, selected) => {
if(event.key === "Enter" || event.key === " "){
event.preventDefault();
handleSubmit(selected);
}
}
useEffect( () => {
//expects options to be an array of dictionaries with a "display" field for what gets presented to the user
const opts = [...props.options];
@@ -89,24 +93,37 @@ export function MythicSelectFromRawListDialog(props) {
return (
<React.Fragment>
<DialogTitle >{props.title}</DialogTitle>
<TableContainer component={Paper} className="mythicElement">
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
<TableBody style={{whiteSpace: "pre"}}>
{options.map( choice => (
<TableRow hover key={choice}>
<TableCell style={{width: "5rem"}}>
<Button onClick={() => handleSubmit(choice)} variant="contained" color="primary">Select</Button>
</TableCell>
<TableCell>{choice}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<DialogContent dividers={true} className="mythic-raw-select-dialog-content">
{options.length === 0 ? (
<Box className="mythic-raw-select-empty items-center flex justify-center text-muted text-center">
<Typography variant="body2">No options available</Typography>
</Box>
) : (
<Stack className="mythic-raw-select-list gap-4">
{options.map( (choice, i) => (
<Box
key={String(choice) + i}
className="mythic-raw-select-row items-center flex gap-6 justify-between min-w-0 w-full rounded cursor-pointer bg-surface-muted border-subtle text-primary"
role="button"
tabIndex={0}
onClick={() => handleSubmit(choice)}
onKeyDown={(event) => handleKeyDown(event, choice)}
>
<Typography className="mythic-raw-select-value min-w-0 truncate whitespace-nowrap" title={String(choice)}>
{String(choice)}
</Typography>
<MythicActionButton className="mythic-raw-select-action" colorMode="always" tone="info" variant="outlined" size="small" tabIndex={-1}>
{actionText}
</MythicActionButton>
</Box>
))}
</Stack>
)}
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
<MythicActionButton colorMode="always" onClick={props.onClose} tone="primary" variant="contained">
Close
</Button>
</MythicActionButton>
</DialogActions>
</React.Fragment>
);
@@ -1,8 +1,7 @@
import React from 'react';
import { styled } from '@mui/material/styles';
import {useTheme} from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import { Link } from '@mui/material';
import {FileDownloadLinkWithAuth} from "../utilities/FileDownloadWithAuth";
const PREFIX = 'MythicSnackDownload';
@@ -60,7 +59,7 @@ const Root = styled('div')((
[`& .${classes.checkIcon}`]: {
fontSize: 20,
color: '#b3b3b3',
color: theme.palette.text.secondary,
paddingRight: 4,
},
@@ -78,10 +77,10 @@ export const MythicSnackDownload = (props) => {
</Typography>
<React.Fragment>
<Typography gutterBottom>File ready for download</Typography>
<Link download={true} href={"/direct/download/" + props.file_id} target="_blank">
<FileDownloadLinkWithAuth color="textPrimary" download={true} href={"/direct/download/" + props.file_id} target="_blank">
Download here
</Link>
</FileDownloadLinkWithAuth>
</React.Fragment>
</Root>
);
};
};
@@ -0,0 +1,192 @@
import React from 'react';
import Box from '@mui/material/Box';
import {MythicActionButton} from './MythicActionButton';
import CircularProgress from '@mui/material/CircularProgress';
import Skeleton from '@mui/material/Skeleton';
import TableCell from '@mui/material/TableCell';
import TableRow from '@mui/material/TableRow';
import Typography from '@mui/material/Typography';
import {alpha, useTheme} from '@mui/material/styles';
import ErrorOutlineOutlinedIcon from '@mui/icons-material/ErrorOutlineOutlined';
import HourglassEmptyOutlinedIcon from '@mui/icons-material/HourglassEmptyOutlined';
import InboxOutlinedIcon from '@mui/icons-material/InboxOutlined';
import SearchOffOutlinedIcon from '@mui/icons-material/SearchOffOutlined';
const stateIcons = {
empty: <InboxOutlinedIcon />,
error: <ErrorOutlineOutlinedIcon />,
loading: <HourglassEmptyOutlinedIcon />,
search: <SearchOffOutlinedIcon />,
};
export function MythicStateDisplay({
action,
actionLabel,
compact = false,
description,
icon,
loading = false,
minHeight,
onAction,
severity = "empty",
sx = {},
title,
}) {
const theme = useTheme();
const defaultIcon = stateIcons[severity] || stateIcons.empty;
const iconNode = loading ? (
<CircularProgress size={compact ? 18 : 22} color="inherit" disableShrink />
) : React.cloneElement(icon || defaultIcon, {fontSize: compact ? "small" : "medium"});
const resolvedTitle = title || (loading ? "Loading" : "Nothing to show");
const resolvedMinHeight = minHeight || (compact ? 112 : 176);
return (
<Box sx={{
alignItems: "center",
color: theme.palette.text.secondary,
display: "flex",
justifyContent: "center",
minHeight: resolvedMinHeight,
p: compact ? 1.5 : 2.5,
width: "100%",
...sx,
}}>
<Box sx={{
alignItems: "center",
display: "flex",
flexDirection: "column",
gap: compact ? 0.75 : 1,
maxWidth: compact ? 360 : 460,
textAlign: "center",
}}>
<Box sx={{
alignItems: "center",
backgroundColor: theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.055) : alpha(theme.palette.common.black, 0.035),
border: `1px solid ${theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.09) : alpha(theme.palette.common.black, 0.08)}`,
borderRadius: "6px",
color: severity === "error" ? theme.palette.error.main : theme.palette.text.secondary,
display: "inline-flex",
height: compact ? 32 : 40,
justifyContent: "center",
width: compact ? 32 : 40,
}}>
{iconNode}
</Box>
<Typography component="div" sx={{
color: theme.palette.text.primary,
fontSize: compact ? "0.86rem" : "0.95rem",
fontWeight: 800,
lineHeight: 1.25,
}}>
{resolvedTitle}
</Typography>
{description &&
<Typography component="div" sx={{
color: theme.palette.text.secondary,
fontSize: compact ? "0.76rem" : "0.82rem",
lineHeight: 1.35,
}}>
{description}
</Typography>
}
{action || (actionLabel && onAction) ? (
action || (
<MythicActionButton size="small" variant="outlined" onClick={onAction} sx={{mt: 0.5}}>
{actionLabel}
</MythicActionButton>
)
) : null}
</Box>
</Box>
);
}
export function MythicEmptyState(props) {
return <MythicStateDisplay severity="empty" {...props} />;
}
export function MythicSearchEmptyState(props) {
return <MythicStateDisplay severity="search" title="No results" {...props} />;
}
export function MythicLoadingState(props) {
return <MythicStateDisplay severity="loading" loading title="Loading" {...props} />;
}
export function MythicErrorState(props) {
return <MythicStateDisplay severity="error" title="Something went wrong" {...props} />;
}
export function MythicTableEmptyState({colSpan, ...props}) {
return (
<TableRow>
<TableCell colSpan={colSpan} sx={{borderBottom: 0, p: 0}}>
<MythicEmptyState {...props} />
</TableCell>
</TableRow>
);
}
export function MythicTableSearchEmptyState({colSpan, ...props}) {
return (
<TableRow>
<TableCell colSpan={colSpan} sx={{borderBottom: 0, p: 0}}>
<MythicSearchEmptyState {...props} />
</TableCell>
</TableRow>
);
}
export function MythicTableErrorState({colSpan, ...props}) {
return (
<TableRow>
<TableCell colSpan={colSpan} sx={{borderBottom: 0, p: 0}}>
<MythicErrorState {...props} />
</TableCell>
</TableRow>
);
}
export function MythicTableSkeletonRows({colSpan, columns = 4, rows = 4}) {
const skeletonColumns = Math.max(1, columns);
return (
<>
{[...Array(rows).keys()].map((rowIndex) => (
<TableRow key={`mythic-table-skeleton-${rowIndex}`}>
<TableCell colSpan={colSpan} sx={{px: 1.25, py: 0.75}}>
<Box sx={{
display: "grid",
gap: 1,
gridTemplateColumns: `repeat(${skeletonColumns}, minmax(0, 1fr))`,
}}>
{[...Array(skeletonColumns).keys()].map((columnIndex) => (
<Skeleton
animation="wave"
height={22}
key={`mythic-table-skeleton-${rowIndex}-${columnIndex}`}
sx={{borderRadius: 1}}
variant="rounded"
width={columnIndex === skeletonColumns - 1 ? "72%" : "100%"}
/>
))}
</Box>
</TableCell>
</TableRow>
))}
</>
);
}
export function MythicTableLoadingState({colSpan, columns, rows = 4, showSkeleton = true, ...props}) {
return (
<>
<TableRow>
<TableCell colSpan={colSpan} sx={{borderBottom: showSkeleton ? undefined : 0, p: 0}}>
<MythicLoadingState {...props} />
</TableCell>
</TableRow>
{showSkeleton &&
<MythicTableSkeletonRows colSpan={colSpan} columns={columns} rows={rows} />
}
</>
);
}
@@ -0,0 +1,123 @@
import React from 'react';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import RadioButtonCheckedIcon from '@mui/icons-material/RadioButtonChecked';
import RadioButtonUncheckedIcon from '@mui/icons-material/RadioButtonUnchecked';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import TimelapseIcon from '@mui/icons-material/Timelapse';
import DoneAllIcon from '@mui/icons-material/DoneAll';
import BlockIcon from '@mui/icons-material/Block';
import HideSourceIcon from '@mui/icons-material/HideSource';
import PanoramaFishEyeIcon from '@mui/icons-material/PanoramaFishEye';
import NotificationsActiveOutlinedIcon from '@mui/icons-material/NotificationsActiveOutlined';
import {MythicChip} from './MythicChip';
import {MythicStyledTooltip} from './MythicStyledTooltip';
const statusConfig = {
success: {tone: "success", icon: <CheckCircleOutlineIcon />, label: "Success", tooltip: "Completed successfully"},
error: {tone: "error", icon: <ErrorOutlineIcon />, label: "Error", tooltip: "Errored"},
warning: {tone: "warning", icon: <WarningAmberIcon />, label: "Warning"},
info: {tone: "info", icon: <InfoOutlinedIcon />, label: "Info"},
active: {tone: "success", icon: <RadioButtonCheckedIcon />, label: "Active"},
inactive: {tone: "error", icon: <RadioButtonUncheckedIcon />, label: "Inactive"},
disabled: {tone: "neutral", icon: <RadioButtonUncheckedIcon />, label: "Disabled"},
deleted: {tone: "error", icon: <DeleteOutlineIcon />, label: "Deleted"},
locked: {tone: "warning", icon: <LockOutlinedIcon />, label: "Locked"},
building: {tone: "warning", icon: <TimelapseIcon />, label: "Building"},
completed: {tone: "success", icon: <DoneAllIcon />, label: "Completed"},
blocked: {tone: "error", icon: <BlockIcon />, label: "Blocked"},
skipped: {tone: "secondary", icon: <HideSourceIcon />, label: "Skipped"},
running: {tone: "info", icon: <TimelapseIcon />, label: "Running"},
cancelled: {tone: "warning", icon: <HideSourceIcon />, label: "Cancelled"},
queued: {tone: "secondary", icon: <PanoramaFishEyeIcon />, label: "Queued", tooltip: "Waiting to run"},
awaiting_approval: {tone: "warning", icon: <TimelapseIcon />, label: "Awaiting approval"},
input_needed: {tone: "warning", icon: <InfoOutlinedIcon />, label: "Input needed"},
configured: {tone: "secondary", icon: <PanoramaFishEyeIcon />, label: "Configured"},
waiting: {tone: "secondary", icon: <PanoramaFishEyeIcon />, label: "Waiting"},
runnable: {tone: "success", icon: <RadioButtonCheckedIcon />, label: "Runnable"},
needs_approval: {tone: "warning", icon: <TimelapseIcon />, label: "Needs approval"},
stopped: {tone: "warning", icon: <RadioButtonUncheckedIcon />, label: "Stopped"},
available: {tone: "success", icon: <RadioButtonCheckedIcon />, label: "Available"},
hidden: {tone: "warning", icon: <HideSourceIcon />, label: "Hidden"},
updating: {tone: "warning", icon: <TimelapseIcon />, label: "Updating"},
alerting: {tone: "warning", icon: <NotificationsActiveOutlinedIcon />, label: "Alerting"},
dead: {tone: "warning", icon: <WarningAmberIcon />, label: "Likely dead"},
unknown: {tone: "error", icon: <ErrorOutlineIcon />, label: "Unknown"},
secondary: {tone: "secondary", icon: <InfoOutlinedIcon />, label: "Status"},
};
export function getMythicStatusConfig(status) {
return statusConfig[status] || statusConfig.secondary;
}
export function getMythicStatusFromTaskStatus(status) {
const normalized = `${status || ""}`.toLowerCase();
if(normalized.includes("error") || normalized.includes("failed")){
return "error";
}
if(normalized.includes("warning")){
return "warning";
}
if(normalized.includes("success") || normalized.includes("completed")){
return "success";
}
if(normalized.includes("processing") || normalized.includes("building")){
return "building";
}
return "info";
}
export function MythicStatusIcon({
className = "",
status = "secondary",
tooltip = false,
tooltipStyle = {},
tooltipTitle,
...props
}) {
const config = getMythicStatusConfig(status);
const icon = React.cloneElement(config.icon, {
...props,
className: `mythic-icon-tone text-tone mythic-tone-${config.tone}${className ? ` ${className}` : ""}`,
});
return tooltip ? (
<MythicStyledTooltip
title={tooltipTitle || config.tooltip || config.label}
tooltipStyle={{display: "inline-flex", ...tooltipStyle}}
>
{icon}
</MythicStyledTooltip>
) : icon;
}
export function MythicStatusChip({
label,
status = "secondary",
className = "",
icon,
showIcon = true,
size = "small",
variant = "soft",
sx = {},
...props
}) {
const config = getMythicStatusConfig(status);
const chipIcon = showIcon ? (icon || config.icon) : undefined;
return (
<MythicChip
className={className}
label={label || config.label}
icon={chipIcon}
size={size}
tone={config.tone}
variant={variant}
sx={sx}
{...props}
/>
);
}
@@ -1,25 +1,17 @@
import React from 'react';
import { Tooltip } from '@mui/material';
import {useTheme} from '@mui/material/styles';
export function MythicStyledTooltip(props){
const { children, title, style, ...other} = props;
export function MythicStyledTooltip({ children, title, enterDelay, tooltipStyle}){
const theme = useTheme();
return (
<Tooltip title={title} arrow followCursor enterDelay={1000} componentsProps={{
tooltip: {
sx: {
backgroundColor: theme.palette.background.contrast,
color: theme.palette.text.contrast,
boxShadow: theme.shadows[1],
fontSize: 13,
'& .MuiTooltip-arrow': {
color: theme.palette.background.contrast
}
}
}
}} style={{zIndex:1000}} {...other}>
{<span style={{...style, display: "inline-block"}}>{children}</span>}
</Tooltip>
);
<span style={{display: "inline-block", ...tooltipStyle}}
data-tooltip-id={"my-tooltip"}
data-tooltip-content={title}
data-tooltip-variant={theme.palette.mode === 'dark' ? 'light' : 'dark'}
data-tooltip-delay-show={enterDelay ? enterDelay : 750}
>
{children}
</span>
)
}
@@ -1,18 +1,22 @@
import { IconButton } from '@mui/material';
import {MythicActionButton} from "./MythicActionButton";
import CloseIcon from '@mui/icons-material/Close';
import Tab from '@mui/material/Tab';
import React from 'react';
import { useCallback } from 'react';
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 Badge from '@mui/material/Badge';
import {alpha, useTheme} from '@mui/material/styles';
import {
Draggable,
} from "@hello-pangea/dnd";
import IconButton from "@mui/material/IconButton";
export function MythicTabPanel(props) {
const { children, value, index, maxHeight, tabInfo, getCallbackData, queryParams, changeSearchParam, ...other } =
const { children, value, index, maxHeight, tabInfo, getCallbackData, queryParams, changeSearchParam, showDeleted, ...other } =
props;
const style =
props.style === undefined
@@ -20,6 +24,7 @@ export function MythicTabPanel(props) {
display: value === index ? 'flex' : 'none',
flexDirection: 'column',
flexGrow: 1,
minHeight: 0,
width: '100%',
maxWidth: '100%',
overflowY: "auto",
@@ -37,57 +42,12 @@ export function MythicTabPanel(props) {
</div>
);
}
function a11yProps(index) {
export function a11yProps(index) {
return {
id: `scrollable-auto-tab-${index}`,
'aria-controls': `scrollable-auto-tabpanel-${index}`,
};
}
function allowDrop(ev) {
ev.preventDefault();
}
function drag(ev) {
ev.dataTransfer.setData("text", ev.target.id);
ev.dataTransfer.setData("opacity", ev.target.style.opacity);
ev.target.style.opacity = "0.2";
}
function onDragEnter(ev){
let node = ev.target.getAttribute("role") === "tab" ? ev.target : ev.target.closest('[role="tab"]');
node.style.borderLeft = "3px dotted red";
//console.log("ondragenter");
}
function onDragLeave(ev){
let node = ev.target.closest('[role="tab"]');
if(ev.target.getAttribute("role") === "tab"){
//console.log("leaving tab", ev.target)
node.style.border = "";
} else if(node.contains(ev.target)){
//console.log("onDragLeave, nodeContains", node, ev.target)
}else{
node.style.border = "";
//console.log("onDragLeave, not contains", node, ev.target)
}
}
function drop(ev) {
// shares dataTransfer with drag function
ev.preventDefault();
const data = ev.dataTransfer.getData("text");
const tabList = ev.target.closest("div[role='tablist']");
let node = ev.target.getAttribute("role") === "tab" ? ev.target : ev.target.closest('[role="tab"]');
//console.log(tabList, data, node.nextSibling);
node.style.border = "";
document.getElementById(data).style.opacity = ev.dataTransfer.getData("opacity");
for(let i = 0; i < tabList.children.length; i++){
tabList.children[i].style.border = "";
}
//tabList.insertBefore(document.getElementById(data), node.nextSibling);
//console.log("selected", data, "toLeftOf", node.id);
return {"selected": data, "toLeftOf": node.id};
}
export function MythicTabLabel(props) {
const {
label,
@@ -103,17 +63,22 @@ export function MythicTabLabel(props) {
onEditTabDescription,
getCallbackData,
onDragTab,
selectedIndex,
highlight,
newDataForTab,
...other
} = props;
const onClick = (e) => {
e.stopPropagation();
e.preventDefault();
onCloseTab({ tabID: tabInfo.tabID, index: index });
};
const theme = useTheme();
const [openContextMenu, setOpenContextMenu] = React.useState(false);
const dropdownAnchorRef = React.useRef(null);
const theme = useTheme();
const handleContextClick = (event) => {
event.preventDefault();
event.stopPropagation();
if(contextMenuOptions && contextMenuOptions.length > 0){
setOpenContextMenu(true);
}
@@ -129,51 +94,75 @@ export function MythicTabLabel(props) {
setOpenContextMenu(false);
};
return (
<Tab
onDrop={(ev) => onDragTab(drop(ev))}
onDragOver={allowDrop}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
draggable={!!onDragTab}
onDragStart={drag}
label={
<span onContextMenu={handleContextClick} style={{ display: 'inline-block', zIndex: 1}} ref={dropdownAnchorRef}>
{label}
<IconButton component='div' size='small' onClick={onClick} {...other}>
<CloseIcon />
</IconButton>
<Draggable draggableId={`callbacks-tab-${tabInfo.tabID}`} index={index} disableInteractiveElementBlocking={true}>
{(provided, snapshot) => (
<div ref={provided.innerRef}
{...provided.draggableProps}>
{(() => {
const selected = selectedIndex === index || snapshot.isDragging;
const tabAccent = tabInfo?.color || theme.palette.primary.main;
const selectedTabSurface = theme.palette.background.paper;
const selectedTabAccent = alpha(tabAccent, theme.palette.mode === "dark" ? 0.34 : 0.2);
return (
<Tab
label={
<span onContextMenu={handleContextClick} style={{}} ref={dropdownAnchorRef}>
<Badge color="success" variant="dot" invisible={!highlight} >
{label}
</Badge>
</span>
}
{...a11yProps(index)}
{...other}
{...provided.dragHandleProps}
style={{
padding: "0rem .5rem 0rem 0.5rem",
margin: "0 0.125rem 0 0",
minHeight: "2rem",
borderRadius: `${theme.shape.borderRadius}px`,
borderTop: selected ? `3px solid ${tabAccent}` : `1px solid ${theme.borderColor}`,
borderRight: `1px solid ${theme.borderColor}`,
borderBottom: `1px solid ${theme.borderColor}`,
borderLeft: `1px solid ${theme.borderColor}`,
backgroundColor: selected ? selectedTabSurface : theme.surfaces?.muted,
backgroundImage: selected ?
`linear-gradient(180deg, ${selectedTabAccent} 0%, ${selectedTabSurface} 82%)` :
"none",
color: theme.palette.text.primary,
}}
/>
);
})()}
<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, color: "white"}}>
<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>
</span>
}
{...a11yProps(index)}
{...other}
style={{padding: "0px 5px 0px 5px"}}
/>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
}}
>
<Paper variant="outlined" className={"dropdownMenuColored bg-surface rounded border"}>
<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>
))}
<MenuItem onClick={onClick}>Close Tab</MenuItem>
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</div>
)}
</Draggable>
);
}
export function MythicSearchTabLabel(props) {
@@ -182,10 +171,9 @@ export function MythicSearchTabLabel(props) {
return (
<Tab
label={
<span>
{label}
<br />
<span className="mythic-search-tab-label items-center inline-flex gap-3 whitespace-nowrap">
{iconComponent}
<span>{label}</span>
</span>
}
{...a11yProps(index)}
@@ -1,6 +1,19 @@
import {styled} from '@mui/material/styles';
import TableCell from '@mui/material/TableCell';
const MythicStyledTableCell = styled(TableCell, {name: "StyledTableRow", slot: "Wrapper"})({
padding: "0 0 0 10px",
});
const MythicStyledTableCell = styled(TableCell, {})(({theme}) => ({
borderBottom: `1px solid ${theme.table?.borderSoft || theme.borderColor}`,
lineHeight: 1.35,
padding: "6px 10px !important",
verticalAlign: "middle",
"&.MuiTableCell-head": {
borderBottom: `1px solid ${theme.table?.border || theme.borderColor}`,
padding: "6px 10px !important",
},
"&:first-of-type": {
paddingLeft: "12px !important",
},
"&:last-of-type": {
paddingRight: "12px !important",
},
}));
export default MythicStyledTableCell;
@@ -0,0 +1,119 @@
import React from 'react';
import Box from '@mui/material/Box';
import Pagination from '@mui/material/Pagination';
import Typography from '@mui/material/Typography';
export const MythicTablePagination = ({
count,
totalCount,
fetchLimit,
onChange,
page,
id,
color = "primary",
label = "Total Results",
summary,
className = "",
containerStyle = {},
paginationStyle = {},
...paginationProps
}) => {
const pageCount = count ?? Math.ceil((totalCount || 0) / (fetchLimit || 1));
const controlledPage = page === undefined ? {} : {page};
return (
<Box className={`mythic-table-footer flex justify-center items-center flex-wrap gap-4 rounded bg-surface-muted border-subtle ${className} w-full`.trim()} id={id} style={containerStyle}>
<Pagination
count={pageCount}
variant="outlined"
color={color}
boundaryCount={1}
siblingCount={1}
onChange={onChange}
showFirstButton={true}
showLastButton={true}
style={paginationStyle}
{...controlledPage}
{...paginationProps}
/>
{summary !== undefined && summary !== null ? (
<Typography className="mythic-table-total text-sm font-650 text-muted whitespace-nowrap">{summary}</Typography>
) : totalCount !== undefined && (
<Typography className="mythic-table-total text-sm font-650 text-muted whitespace-nowrap">{label}: {totalCount}</Typography>
)}
</Box>
);
};
export const useMythicClientPagination = ({
items = [],
resetKey = "",
rowsPerPage = 25,
}) => {
const safeItems = React.useMemo(() => {
return Array.isArray(items) ? items : [];
}, [items]);
const [page, setPage] = React.useState(1);
const totalCount = safeItems.length;
const pageCount = Math.max(1, Math.ceil(totalCount / rowsPerPage));
const safePage = Math.min(page, pageCount);
const pageStart = (safePage - 1) * rowsPerPage;
const pageData = React.useMemo(() => {
return safeItems.slice(pageStart, pageStart + rowsPerPage);
}, [pageStart, rowsPerPage, safeItems]);
const pageEnd = totalCount === 0 ? 0 : Math.min(pageStart + rowsPerPage, totalCount);
const pageStartDisplay = totalCount === 0 ? 0 : pageStart + 1;
React.useEffect(() => {
setPage(1);
}, [resetKey, rowsPerPage, totalCount]);
React.useEffect(() => {
if(page > pageCount){
setPage(pageCount);
}
}, [page, pageCount]);
const onChangePage = React.useCallback((event, nextPage) => {
setPage(nextPage);
}, []);
return {
onChangePage,
page: safePage,
pageCount,
pageData,
pageEnd,
pageStart,
pageStartDisplay,
rangeLabel: `${pageStartDisplay}-${pageEnd} of ${totalCount}`,
rowsPerPage,
showPagination: totalCount > rowsPerPage,
totalCount,
};
};
export const MythicClientSideTablePagination = ({
id,
pagination,
selectedCount,
selectedLabel = "selected",
...paginationProps
}) => {
if(!pagination?.showPagination){
return null;
}
const summary = selectedCount === undefined || selectedCount === null ?
pagination.rangeLabel :
`${pagination.rangeLabel}, ${selectedCount} ${selectedLabel}`;
return (
<MythicTablePagination
count={pagination.pageCount}
id={id}
onChange={pagination.onChangePage}
page={pagination.page}
summary={summary}
{...paginationProps}
/>
);
};
@@ -0,0 +1,116 @@
import React from 'react';
import Box from '@mui/material/Box';
import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select';
import ToggleButton from '@mui/material/ToggleButton';
import SearchIcon from '@mui/icons-material/Search';
import MythicTextField from './MythicTextField';
import {MythicActionButton} from './MythicActionButton';
export const MythicTableToolbar = ({children, className = "", style = {}, variant}) => {
return (
<Box className={`mythic-table-toolbar flex flex-none flex-wrap gap-4 w-full rounded bg-surface-muted border-subtle${variant ? ` mythic-table-toolbar-${variant}` : ""} ${className}`.trim()} style={style}>
{children}
</Box>
);
};
export const MythicTableToolbarGroup = ({children, grow = false, label, className = "", style = {}}) => {
return (
<Box className={`mythic-table-toolbar-group items-center flex flex-wrap gap-3 ${grow ? "mythic-table-toolbar-group-grow" : ""} ${className} max-w-full`.trim()} style={style}>
{label &&
<span className="mythic-table-toolbar-group-label text-2xs font-800 leading-100 text-muted">{label}</span>
}
{children}
</Box>
);
};
export const MythicToolbarSelect = ({children, className = "", style = {}, ...props}) => {
return (
<Select
className={`mythic-toolbar-select ${className} w-full`.trim()}
size="small"
style={style}
{...props}
>
{children}
</Select>
);
};
export const MythicToolbarMenuItem = MenuItem;
export const MythicSearchField = ({
value,
onChange,
onEnter,
onSearch,
disabled = false,
placeholder = "Search...",
name = "Search",
showLabel = false,
autoFocus,
inputProps = {},
endAdornment = null,
}) => {
return (
<MythicTextField
disabled={disabled}
placeholder={placeholder}
value={value}
marginTop="0px"
marginBottom="0px"
showLabel={showLabel}
onChange={onChange}
onEnter={onEnter || onSearch}
name={name}
autoFocus={autoFocus}
InputProps={{
endAdornment: onSearch ? (
<React.Fragment>
{endAdornment}
<MythicActionButton disabled={disabled} icon={<SearchIcon />} iconOnly onClick={onSearch} tone="primary" tooltip="Search" />
</React.Fragment>
) : endAdornment,
style: {padding: 0},
...inputProps,
}}
/>
);
};
export const MythicToolbarButton = ({children, tone, ...props}) => {
return (
<MythicActionButton tone={tone} {...props}>
{children}
</MythicActionButton>
);
};
export const MythicToolbarToggle = ({
checked,
onClick,
label,
activeLabel,
inactiveLabel,
activeIcon,
inactiveIcon,
value = "toggle",
className = "",
...props
}) => {
return (
<ToggleButton
className={`mythic-toolbar-toggle border-subtle text-muted gap-3 rounded whitespace-nowrap ${className}`.trim()}
value={value}
selected={checked}
onClick={onClick}
size="small"
{...props}
>
{checked ? activeIcon : inactiveIcon}
<span>{checked ? (activeLabel || label) : (inactiveLabel || label)}</span>
</ToggleButton>
);
};
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,28 @@
import React from 'react';
import {useTheme} from '@mui/material/styles';
import {getReadableTextColor, isValidHexColor} from './MythicColorInput';
import {MythicChip} from './MythicChip';
export const getTagReadableTextColor = (theme, color) => {
return isValidHexColor(color) ? getReadableTextColor(color) : theme.palette.text.primary;
}
export const TagTypeChip = ({tagtype, label, sx={}}) => {
const theme = useTheme();
const color = tagtype?.color || "";
const textColor = getTagReadableTextColor(theme, color);
return (
<MythicChip
color={color}
label={label || tagtype?.name || "Tag"}
size="small"
sx={{
backgroundColor: color || "transparent",
border: "1px solid",
borderColor: theme.borderColor,
color: textColor,
...sx,
}}
/>
);
}
@@ -1,5 +1,5 @@
import React, {useState} from 'react';
import Button from '@mui/material/Button';
import {MythicActionButton} from './MythicActionButton';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
@@ -15,7 +15,7 @@ export function MythicTextEditDialog(props) {
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">{props.title}</DialogTitle>
<DialogContent dividers={true}>
<DialogContent dividers={true} style={{padding: 0}}>
<AceEditor
mode="json"
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
@@ -24,6 +24,8 @@ export function MythicTextEditDialog(props) {
showGutter={true}
highlightActiveLine={true}
value={props.value}
showPrintMargin={false}
wrapEnabled={true}
focus={true}
width={"100%"}
setOptions={{
@@ -33,11 +35,10 @@ export function MythicTextEditDialog(props) {
}}/>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={props.onClose} color="primary">
<MythicActionButton colorMode="always" variant="contained" onClick={props.onClose} tone="primary">
Close
</Button>
</MythicActionButton>
</DialogActions>
</React.Fragment>
);
}
@@ -1,104 +1,149 @@
import React from 'react';
import { styled } from '@mui/material/styles';
import PropTypes from 'prop-types';
import {TextField} from '@mui/material';
import {useDebounce} from "../utilities/useDebounce";
const PREFIX = 'MythicTextField';
const classes = {
root: `${PREFIX}-root`
root: `${PREFIX}Div-root`,
textFieldRoot: `${PREFIX}-root`
};
const Root = styled('div')({
const Root = styled('div')(({theme}) => ({
[`&.${classes.root}`]: {
'& input:valid + fieldset': {
borderColor: 'grey',
borderWidth: 1,
},
}));
const ValidationTextField = styled(TextField)(({theme}) => ({
[`&.${classes.textFieldRoot}`]: {
'& .MuiInputBase-root': {
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadius,
},
'& fieldset': {
borderColor: theme.borderColor,
borderWidth: 1,
},
'&:hover fieldset': {
borderColor: theme.palette.text.secondary,
},
'& .Mui-focused fieldset': {
borderColor: `${theme.palette.primary.main} !important`,
borderWidth: '1px !important',
},
'& input:invalid + fieldset': {
borderColor: 'red',
borderWidth: 2,
borderColor: theme.palette.error.main,
borderWidth: 2,
},
'& input:valid:focus + fieldset': {
borderLeftWidth: 6,
padding: '4px !important', // override inline-style
borderLeftWidth: 2,
padding: '4px !important', // override inline-style
},
},
});
'& textarea:focus + textarea + fieldset': {
borderLeftWidth: 2,
//padding: '4px !important', // override inline-style
},
},
}));
const ValidationTextField = TextField;
class MythicTextField extends React.Component {
static propTypes = {
placeholder: PropTypes.string,
name: PropTypes.string,
validate: PropTypes.func,
width: PropTypes.number,
onChange: PropTypes.func.isRequired,
requiredValue: PropTypes.bool,
type: PropTypes.string,
onEnter: PropTypes.func,
autoFocus: PropTypes.bool,
autoComplete: PropTypes.bool,
showLabel: PropTypes.bool,
variant: PropTypes.string,
inline: PropTypes.bool,
marginBottom: PropTypes.string,
}
onChange = evt => {
const name = this.props.name;
const value = evt.target.value;
const error = this.props.validate ? this.props.validate(value) : false;
this.props.onChange(name, value, error, evt);
}
checkError = () => {
return this.props.validate ? this.props.validate(this.props.value) : false
}
onKeyPress = (event) => {
if(event.key === "Enter"){
if(this.props.onEnter !== undefined){
event.stopPropagation();
event.preventDefault();
this.props.onEnter(event);
}
const MythicTextField = ({
placeholder,
name,
validate,
width,
onChange,
requiredValue=false,
type = "text",
onEnter,
autoFocus,
autoComplete=false,
showLabel = true,
variant = "outlined",
inline,
marginBottom = "5px",
value,
disabled = false,
marginTop = "5px",
InputProps = {},
inputProps = {},
inputLabelProps = {},
multiline = false,
maxRows = 10,
errorText = "",
helperText = "",
debounceDelay = 100,
}) => {
const [localValue, setLocalValue] = React.useState({value: value, event: null});
const [localError, setLocalError] = React.useState(false);
const debouncedLocalInput = useDebounce(localValue, debounceDelay);
React.useEffect( () => {
const error = validate ? validate(debouncedLocalInput.value) : false;
onChange(name, debouncedLocalInput.value, error, debouncedLocalInput.event);
}, [debouncedLocalInput]);
React.useEffect( () => {
setLocalValue({value: value, event: null});
}, [value]);
const handleChange = (evt) => {
const newValue = evt.target.value;
// Update local state immediately for responsive UI
setLocalValue({value: newValue, event: evt});
setLocalError(validate ? validate(newValue) : false);
};
const onKeyPress = (event) => {
if(event.key === "Enter") {
if(event.shiftKey){
handleChange(event);
return;
}
if (onEnter !== undefined) {
event.stopPropagation();
event.preventDefault();
onEnter(event);
}
}else{
this.onChange(event);
handleChange(event);
}
}
render(){
return (
<Root style={{width: this.props.width ? this.props.width + "rem" : "100%", display: this.props.inline ? "inline-block": "",}}>
<ValidationTextField
fullWidth={true}
placeholder={this.props.placeholder}
value={this.props.value}
onChange={this.onChange}
onKeyDown={this.onKeyPress}
label={this.props.showLabel === undefined ? this.props.name : this.props.showLabel ? this.props.name : undefined}
autoFocus={this.props.autoFocus}
variant={this.props.variant === undefined ? "outlined" : this.props.variant}
data-lpignore={true}
autoComplete={this.props.autoComplete === undefined ? "off" : (this.props.autoComplete ? "on" : "off")}
disabled={this.props.disabled === undefined ? false : this.props.disabled}
required={this.props.requiredValue ? this.props.requiredValue : false}
InputLabelProps={this.props.inputLabelProps}
multiline={this.props.multiline ? this.props.multiline : false}
maxRows={this.props.maxRows}
error={this.checkError()}
type={this.props.type === undefined ? "text" : this.props.type}
onWheel={ event => event.target.blur() }
InputProps={this.props.InputProps}
helperText={this.checkError() ? this.props.errorText : this.props.helperText}
style={{
padding:0,
marginBottom: this.props.marginBottom ? this.props.marginBottom : "10px",
display: this.props.inline ? "inline-block": "",
}}
classes={{
root: classes.root
}} />
</Root>
);
}
const resolvedAutoComplete = autoComplete === undefined ? "new-password" : (
typeof autoComplete === "string" ? autoComplete : (autoComplete ? "on" : "off")
);
return (
<Root style={{width: width ? width + "rem" : "100%", display: inline ? "inline-block": "",}}>
<ValidationTextField
fullWidth={true}
placeholder={placeholder}
value={localValue.value}
onChange={handleChange}
color={"secondary"}
onKeyDown={onKeyPress}
label={showLabel ? name : undefined}
autoFocus={autoFocus}
variant={variant}
data-lpignore={true}
autoComplete={resolvedAutoComplete}
disabled={disabled}
required={requiredValue}
InputLabelProps={inputLabelProps}
multiline={multiline}
maxRows={maxRows}
error={localError}
type={type}
onWheel={ event => event.target.blur() }
InputProps={{...InputProps, spellCheck: false}}
inputProps={{...inputProps, autoComplete: resolvedAutoComplete}}
helperText={localError ? errorText : helperText}
style={{
padding:0,
marginBottom: marginBottom ? marginBottom : "5px",
marginTop: marginTop ? marginTop: "5px",
display: inline ? "inline-block": "",
}}
classes={{
root: classes.textFieldRoot
}} />
</Root>
);
}
export default MythicTextField;
@@ -0,0 +1,62 @@
import * as React from "react";
import { useState, useEffect } from "react";
import { MythicStyledTooltip } from './MythicStyledTooltip';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import utc from 'dayjs/plugin/utc';
dayjs.extend(relativeTime);
dayjs.extend(utc);
const formatDiff = (ms) => {
if (ms < 0) ms = 0;
const s = Math.trunc(ms / 1000) % 60;
const m = Math.trunc(ms / 60000) % 60;
const h = Math.trunc(ms / 3600000) % 24;
const d = Math.trunc(ms / 86400000);
return (d ? d + "d " : "") + (h ? h + "h " : "") + (m ? m + "m " : "") + s + "s";
};
export const MythicTimestamp = ({
children,
fromNow = false,
duration = false,
endTime,
interval = 0,
withTitle = false,
titleFormat = "YYYY-MM-DD HH:mm:ss",
serverSkew = 0,
}) => {
const [now, setNow] = useState(Date.now);
useEffect(() => {
if (!interval) return;
const id = setInterval(() => setNow(Date.now()), interval);
return () => clearInterval(id);
}, [interval]);
// Mythic's server timestamps without an explicit offset represent UTC.
const t = dayjs.utc(children);
const compute = () => {
if (!t.isValid()) return "";
const serverNow = dayjs(now).add(serverSkew, 'ms');
if (duration) {
const end = endTime != null ? dayjs.utc(endTime) : serverNow;
if (!end.isValid()) return "";
return formatDiff(end.diff(t));
}
if (fromNow) {
const seconds = Math.round(Math.abs(serverNow.diff(t)) / 1000);
if (seconds < 60) return `${seconds} ${seconds === 1 ? 'second' : 'seconds'}`;
return t.from(serverNow, true);
}
return t.local().format(titleFormat);
};
const display = compute();
const title = withTitle && t.isValid() ? t.local().format(titleFormat) : "";
if (withTitle) return (
<MythicStyledTooltip title={title}>
<span>{display}</span>
</MythicStyledTooltip>
);
return <span>{display}</span>;
};
@@ -0,0 +1,89 @@
import React, {act} from 'react';
import {createRoot} from 'react-dom/client';
import {MythicTimestamp} from './MythicTimestamp';
jest.mock('./MythicStyledTooltip', () => ({
MythicStyledTooltip: ({children, title}) => <div title={title}>{children}</div>,
}));
describe('MythicTimestamp', () => {
let container;
let root;
const start = '2026-01-15T12:00:00.000000';
const render = (props) => {
act(() => root.render(<MythicTimestamp {...props} />));
return container.textContent;
};
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2026-01-15T12:00:10Z'));
global.IS_REACT_ACT_ENVIRONMENT = true;
container = document.createElement('div');
root = createRoot(container);
});
afterEach(() => {
act(() => root.unmount());
expect(jest.getTimerCount()).toBe(0);
jest.useRealTimers();
delete global.IS_REACT_ACT_ENVIRONMENT;
});
it('uses UTC for timezone-less end timestamps and respects explicit offsets', () => {
const props = {children: start + 'Z', duration: true};
expect(render({...props, endTime: '2026-01-15T12:01:00.000000'})).toBe('1m 0s');
expect(render({...props, endTime: '2026-01-15T04:01:00-08:00'})).toBe('1m 0s');
});
it('recomputes when only the end time changes', () => {
const props = {children: start + 'Z', duration: true};
render({...props, endTime: '2026-01-15T12:01:00'});
expect(render({...props, endTime: '2026-01-15T12:02:00'})).toBe('2m 0s');
});
it('uses updated server skew immediately and on subsequent ticks', () => {
const props = {children: start + 'Z', fromNow: true, interval: 1000};
expect(render(props)).toBe('10 seconds');
expect(render({...props, serverSkew: 3600000})).toBe('an hour');
act(() => jest.advanceTimersByTime(1000));
expect(container.textContent).toBe('an hour');
});
it('keeps numeric seconds through 59 seconds and updates every tick', () => {
const props = {children: start + 'Z', fromNow: true, interval: 1000};
expect(render(props)).toBe('10 seconds');
act(() => jest.advanceTimersByTime(49000));
expect(container.textContent).toBe('59 seconds');
act(() => jest.advanceTimersByTime(1000));
expect(container.textContent).toBe('a minute');
});
it('shows zero seconds for fresh check-ins, including after the age resets', () => {
const props = {fromNow: true, interval: 1000};
expect(render({...props, children: new Date(Date.now() - 200).toISOString()})).toBe('0 seconds');
act(() => jest.advanceTimersByTime(1000));
expect(container.textContent).toBe('1 second');
act(() => jest.advanceTimersByTime(1000));
expect(container.textContent).toBe('2 seconds');
expect(render({...props, children: new Date(Date.now()).toISOString()})).toBe('0 seconds');
act(() => jest.advanceTimersByTime(1000));
expect(container.textContent).toBe('1 second');
});
it('recomputes when display mode or format changes', () => {
const props = {children: start + 'Z'};
expect(render({...props, fromNow: true})).toBe('10 seconds');
expect(render({...props, titleFormat: 'YYYY'})).toBe('2026');
expect(render({...props, titleFormat: 'MM'})).toBe('01');
});
it('renders invalid timestamps as empty text and clears timers on unmount', () => {
expect(render({children: 'invalid', fromNow: true, interval: 1000})).toBe('');
expect(jest.getTimerCount()).toBe(1);
render({children: start, duration: true, endTime: 'invalid'});
expect(container.textContent).toBe('');
expect(jest.getTimerCount()).toBe(0);
});
});
@@ -1,6 +1,6 @@
import React, {useEffect} from 'react';
import { styled } from '@mui/material/styles';
import Button from '@mui/material/Button';
import {MythicActionButton} from './MythicActionButton';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
@@ -9,14 +9,11 @@ import ListItem from '@mui/material/ListItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import Checkbox from '@mui/material/Checkbox';
import { CardContent } from '@mui/material';
import CardHeader from '@mui/material/CardHeader';
import Divider from '@mui/material/Divider';
const PREFIX = 'MythicTransferList';
export const classes = {
button: `${PREFIX}-button`,
divider: `${PREFIX}-divider`
};
@@ -26,21 +23,10 @@ export const StyledDivider = styled(Divider)((
}
) => ({
[`&.${classes.divider}`]: {
backgroundColor: "rgb(100, 170, 204)",
border: "2px solid rgba(100, 170, 204)"
backgroundColor: theme.palette.primary.main,
border: `2px solid ${theme.palette.primary.main}`
}
}));
export const StyledButton = styled(Button)((
{
theme
}
) => ({
[`&.${classes.button}`]: {
margin: theme.spacing(0.5, 0),
},
}));
const CustomListElement = ({value, onClick, itemKey}) => {
const displayValue = itemKey ? value[itemKey] : value.value;
const labelId = `transfer-list-item-${displayValue}-label`;
@@ -61,10 +47,9 @@ const CustomListElement = ({value, onClick, itemKey}) => {
const CustomList = ({title, items, left, onClick, itemKey}) => {
return (
<>
<CardHeader title={title} />
<StyledDivider classes={{root: classes.divider}}/>
<CardContent style={{flexGrow: 1, height: "100%", width: "100%", overflowY: "auto", padding: 0}}>
<div className="mythic-transfer-list flex flex-column min-w-0 overflow-hidden w-full rounded bg-surface border-subtle">
<div className="mythic-transfer-list-header bg-table-header text-xs font-700 leading-125 border-b text-primary">{title}</div>
<div className="mythic-transfer-list-body flex-fill overflow-auto">
<List dense component="div" role="list" style={{padding:0, width: "100%"}}>
{items.map((value, index) => (
<div key={index}>
@@ -80,8 +65,8 @@ const CustomList = ({title, items, left, onClick, itemKey}) => {
))}
</List>
</CardContent>
</>
</div>
</div>
);
}
const CustomTransferList = ({leftTitle, rightTitle, initialData, parentLeftData, parentRightData, itemKey}) => {
@@ -148,45 +133,49 @@ const CustomTransferList = ({leftTitle, rightTitle, initialData, parentLeftData,
<div style={{paddingLeft: 0, flexGrow: 1, marginLeft: 0, marginRight: "10px", position: "relative", overflowY: "auto", display: "flex", flexDirection: "column" }}>
<CustomList title={leftTitle} left={true} items={data} onClick={handleToggle} itemKey={itemKey} />
</div>
<div style={{display: "flex", flexDirection: "column", justifyContent: "center"}}>
<StyledButton
<div className="mythic-transfer-controls py-0 px-4 items-center flex flex-column gap-6 justify-center">
<MythicActionButton
colorMode="always"
tone="primary"
variant="contained"
size="small"
className={classes.button}
onClick={handleAllRight}
aria-label="move all right"
>
&gt;&gt;
</StyledButton>
<StyledButton
</MythicActionButton>
<MythicActionButton
colorMode="always"
tone="primary"
variant="contained"
size="small"
disabled={data.filter( x => x.checked && x.left).length === 0}
className={classes.button}
onClick={handleCheckedRight}
aria-label="move selected right"
>
&gt;
</StyledButton>
<StyledButton
</MythicActionButton>
<MythicActionButton
colorMode="always"
tone="primary"
variant="contained"
size="small"
disabled={data.filter( x => x.checked && x.right).length === 0}
className={classes.button}
onClick={handleCheckedLeft}
aria-label="move selected left"
>
&lt;
</StyledButton>
<StyledButton
</MythicActionButton>
<MythicActionButton
colorMode="always"
tone="primary"
variant="contained"
size="small"
className={classes.button}
onClick={handleAllLeft}
aria-label="move all left"
>
&lt;&lt;
</StyledButton>
</MythicActionButton>
</div>
<div style={{marginLeft: "10px", position: "relative", flexGrow: 1, display: "flex", overflowY: "auto", flexDirection: "column" }}>
@@ -243,14 +232,13 @@ export function MythicTransferListDialog(props) {
/>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
<MythicActionButton colorMode="always" onClick={props.onClose} tone="primary" variant="contained">
Close
</Button>
<Button onClick={setFinalTags} variant="contained" color="success">
</MythicActionButton>
<MythicActionButton colorMode="always" onClick={setFinalTags} tone="success" variant="contained">
Submit
</Button>
</MythicActionButton>
</DialogActions>
</>
);
}
@@ -0,0 +1,33 @@
import React from 'react';
import {MythicActionButton} from './MythicActionButton';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import 'ace-builds/src-noconflict/mode-json';
import 'ace-builds/src-noconflict/theme-monokai';
import 'ace-builds/src-noconflict/theme-xcode';
import {ResponseDisplayMedia} from "../pages/Callbacks/ResponseDisplayMedia";
import {MythicDraggableDialogTitle} from "./MythicDraggableDialogTitle";
export function PreviewFileMediaDialog({agent_file_id, filename, onClose, editable=false}) {
const onClick = (e) => {
if(e){
e.preventDefault();
e.stopPropagation();
}
}
return (
<React.Fragment>
<MythicDraggableDialogTitle>
Previewing <b>{filename}</b>
</MythicDraggableDialogTitle>
<DialogContent onClick={onClick} style={{height: "calc(80vh)", margin: 0, padding: 0}}>
<ResponseDisplayMedia media={{agent_file_id, filename, editable}} expand={true} />
</DialogContent>
<DialogActions>
<MythicActionButton colorMode="always" variant="contained" onClick={onClose} tone="primary">
Close
</MythicActionButton>
</DialogActions>
</React.Fragment>
);
}
@@ -5,7 +5,7 @@ import DialogTitle from '@mui/material/DialogTitle';
import {milisecondsToString, useInterval } from './utilities/Time';
import {JWTTimeLeft, GetNewToken} from '../index';
import {FailedRefresh} from '../cache';
import { Button } from '@mui/material';
import {MythicActionButton} from './MythicComponents/MythicActionButton';
import { snackActions } from './utilities/Snackbar';
@@ -43,14 +43,13 @@ export function RefreshTokenDialog(props) {
Your Session will expire in {timeLeft}
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
<MythicActionButton colorMode="always" onClick={onClose} tone="primary" variant="contained">
Logout
</Button>
<Button onClick={onCommitSubmit} variant="contained" color="success">
</MythicActionButton>
<MythicActionButton colorMode="always" onClick={onCommitSubmit} tone="success" variant="contained">
Extend
</Button>
</MythicActionButton>
</DialogActions>
</React.Fragment>
)
}
-567
View File
@@ -1,567 +0,0 @@
import React from 'react';
import { styled } from '@mui/material/styles';
import clsx from 'clsx';
import { useTheme } from '@mui/material/styles';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import IconButton from '@mui/material/IconButton';
import MenuIcon from '@mui/icons-material/Menu';
import MenuItem from '@mui/material/MenuItem';
import ManageAccountsIcon from '@mui/icons-material/ManageAccounts';
import Menu from '@mui/material/Menu';
import { Link } from 'react-router-dom';
import Drawer from '@mui/material/Drawer';
import List from '@mui/material/List';
import Divider from '@mui/material/Divider';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import ListItem from '@mui/material/ListItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import { useReactiveVar } from '@apollo/client';
import { menuOpen, FailedRefresh } from '../cache';
import { TopAppBarNotifications } from './TopAppBarNotifications';
import { EventFeedNotifications } from './EventFeedNotifications';
import HelpIcon from '@mui/icons-material/Help';
import PhoneCallbackIcon from '@mui/icons-material/PhoneCallback';
import {ReactComponent as ReactLogo} from './mythic_red_small.svg';
import SpaceDashboardTwoToneIcon from '@mui/icons-material/SpaceDashboardTwoTone';
import ListSubheader from '@mui/material/ListSubheader';
import Collapse from '@mui/material/Collapse';
import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore';
import LayersTwoToneIcon from '@mui/icons-material/LayersTwoTone';
import TableChartTwoToneIcon from '@mui/icons-material/TableChartTwoTone';
import PostAddIcon from '@mui/icons-material/PostAdd';
import SupervisorAccountIcon from '@mui/icons-material/SupervisorAccount';
import EditIcon from '@mui/icons-material/Edit';
import { Typography } from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import BarChartIcon from '@mui/icons-material/BarChart';
import HeadsetTwoToneIcon from '@mui/icons-material/HeadsetTwoTone';
import CodeIcon from '@mui/icons-material/Code';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faBiohazard, faFlagCheckered} from '@fortawesome/free-solid-svg-icons';
import AttachmentIcon from '@mui/icons-material/Attachment';
import FingerprintIcon from '@mui/icons-material/Fingerprint';
import {faSocks} from '@fortawesome/free-solid-svg-icons';
import CameraAltTwoToneIcon from '@mui/icons-material/CameraAltTwoTone';
import {mythicVersion, mythicUIVersion} from '../index';
import {MythicStyledTooltip} from './MythicComponents/MythicStyledTooltip';
import VpnKeyIcon from '@mui/icons-material/VpnKey';
import ThumbDownTwoTone from '@mui/icons-material/ThumbDownTwoTone';
import { MythicDialog } from './MythicComponents/MythicDialog';
import {MythicFeedbackDialog} from './MythicComponents/MythicFeedbackDialog';
import LocalOfferTwoToneIcon from '@mui/icons-material/LocalOfferTwoTone';
import StorageIcon from '@mui/icons-material/Storage';
import PublicIcon from '@mui/icons-material/Public';
import LightModeTwoToneIcon from '@mui/icons-material/LightModeTwoTone';
import DarkModeTwoToneIcon from '@mui/icons-material/DarkModeTwoTone';
const PREFIX = 'TopAppBar';
const classes = {
root: `${PREFIX}-root`,
title: `${PREFIX}-title`,
hide: `${PREFIX}-hide`,
drawer: `${PREFIX}-drawer`,
drawerPaper: `${PREFIX}-drawerPaper`,
drawerHeader: `${PREFIX}-drawerHeader`,
listSubHeader: `${PREFIX}-listSubHeader`,
appBar: `${PREFIX}-appBar`,
appBarShift: `${PREFIX}-appBarShift`,
nested: `${PREFIX}-nested`,
mythicElement: `${PREFIX}-mythicElement`
};
const StyledAppBar = styled(AppBar)((
{
theme
}
) => ({
[`& .${classes.hide}`]: {
display: 'none',
},
[`&.${classes.appBar}`]: {
width: "100%",
backgroundColor: theme.topAppBarColor,
transition: theme.transitions.create(['margin', 'width'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
},
[`&.${classes.appBarShift}`]: {
maxWidth: `calc(100% - ${drawerWidth}px)`,
marginLeft: drawerWidth,
transition: theme.transitions.create(['margin', 'width'], {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
},
}));
const StyledDrawer = styled(Drawer)((
{
theme
}
) => ({
[`& .${classes.title}`]: {
flexGrow: 1,
},
[`&.${classes.drawer}`]: {
width: drawerWidth,
flexShrink: 0,
},
[`& .${classes.drawerPaper}`]: {
width: drawerWidth,
},
[`& .${classes.drawerHeader}`]: {
display: 'flex',
alignItems: 'center',
padding: theme.spacing(0, 1),
justifyContent: 'flex-end',
},
[`& .${classes.listSubHeader}`]: {
backgroundColor: theme.palette.listSubHeader.default
},
[`& .${classes.nested}`]: {
paddingLeft: theme.spacing(4),
},
}));
const drawerWidth = 240;
export function TopAppBar(props) {
const theme = useTheme();
const feedbackRef = React.useRef(null);
const settingsRef = React.useRef(null);
const documentationRef = React.useRef(null);
const [anchorEl, setAnchorEl] = React.useState(null);
const [documentationAnchorEl, setDocumentationAnchorEl] = React.useState(null);
const me = props.me;
const isOpen = useReactiveVar(menuOpen);
const [openGlobal, setOpenGlobal] = React.useState(false);
const [openCreate, setOpenCreate] = React.useState(false);
const [openOperations, setOpenOperations] = React.useState(false);
const [openData, setOpenData] = React.useState(false);
const [openFeedbackForm, setOpenFeedbackForm] = React.useState(false);
const handleDrawerOpen = () => {
menuOpen(true);
};
const handleDrawerClose = () => {
menuOpen(false);
};
const handleMenu = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = (evt) => {
setAnchorEl(null);
};
const handleDocumentationMenu = (event) => {
setDocumentationAnchorEl(event.currentTarget);
};
const handleDocumentationClose = (evt) => {
setDocumentationAnchorEl(null);
};
const handleToggleGlobal = () => {
setOpenGlobal(!openGlobal);
}
const handleToggleCreate = () => {
setOpenCreate(!openCreate);
}
const handleToggleOperations = () => {
setOpenOperations(!openOperations);
}
const handleToggleData = () => {
setOpenData(!openData);
}
const handleLogout = () => {
menuOpen(false);
console.log("clicked logout, calling FailedRefresh");
FailedRefresh();
}
return (
<>
{me?.user?.current_operation_id ? (<EventFeedNotifications me={me} />) : null }
<StyledAppBar className={clsx(classes.appBar, {[classes.appBarShift]: isOpen,})}>
{ me?.loggedIn ? (
<Toolbar variant="dense" >
<IconButton
edge="start"
className={clsx(isOpen && classes.hide)}
color="inherit"
aria-label="menu"
disableFocusRipple={true}
disableRipple={true}
onClick={handleDrawerOpen}
>
<MenuIcon fontSize={"large"} />
</IconButton>
<div style={{width: "100%"}}>
<MythicStyledTooltip title="C2 Profiles and Payload Types">
<IconButton component={Link} to='/new/payloadtypes' color="inherit" size="large" disableFocusRipple={true}
disableRipple={true}>
<HeadsetTwoToneIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Payloads">
<IconButton component={Link} to='/new/payloads' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<FontAwesomeIcon size={"lg"} icon={faBiohazard} />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Search Operation">
<IconButton component={Link} to='/new/search?tab=callbacks&searchField=Host&search=' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<SearchIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Files">
<IconButton
component={Link}
to='/new/search?searchField=Filename&tab=files&location=Downloads'
color="inherit"
disableFocusRipple={true}
disableRipple={true}>
<AttachmentIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Artifacts">
<IconButton
disableFocusRipple={true}
disableRipple={true}
component={Link}
to='/new/search?searchField=Artifact&tab=artifacts'
color="inherit">
<FingerprintIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Proxies">
<IconButton component={Link} to='/new/search?tab=socks' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<FontAwesomeIcon size={"lg"} icon={faSocks} />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Screenshots" arrow >
<IconButton
component={Link}
to='/new/search?searchField=Filename&tab=files&location=Screenshots'
color="inherit"
disableFocusRipple={true}
disableRipple={true}
>
<CameraAltTwoToneIcon fontSize={"large"} />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Credentials" arrow >
<IconButton
component={Link}
to='/new/search?searchField=Account&tab=credentials'
color="inherit"
disableFocusRipple={true}
disableRipple={true}>
<VpnKeyIcon fontSize={"large"} className="mythicElement" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Active Callbacks">
<IconButton component={Link} to='/new/callbacks' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<PhoneCallbackIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Reporting" >
<IconButton component={Link} to='/new/reporting' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<FontAwesomeIcon size={"lg"} icon={faFlagCheckered} />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="MITRE ATT&CK" >
<IconButton component={Link} to='/new/mitre' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<TableChartTwoToneIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Operation Tags" >
<IconButton component={Link} to='/new/tagtypes' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<LocalOfferTwoToneIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<Link style={{display: "inline-flex", alignItems: "center", paddingRight: "10px", color: "white", textDecoration: "none"}} to="/new/operations">
{me?.user?.current_operation || "No Operation Set"}
</Link>
<Menu
id="menu-appbar"
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
open={Boolean(anchorEl)}
onClose={handleClose}
MenuListProps={{style: {backgroundColor: theme.palette.primary.main, color: "white"}}}
>
<MenuItem divider={true} style={{display: "block"}} component={Link} to="/new/settings" onClick={handleClose} name="settings">
<Typography paragraph={true} variant="caption" style={{marginBottom: "0", color: "white"}}>Signed in as:</Typography>
<Typography paragraph={true} variant="body1" style={{marginBottom: "0", fontWeight: 600, color: "white"}}> {me?.user?.username || "" } </Typography>
</MenuItem>
<MenuItem component={Link} to="/new/login" onClick={handleLogout}>Logout</MenuItem>
</Menu>
<div style={{display: "inline-flex", justifyContent: "flex-end", float: "right"}}>
<IconButton
aria-label="feedback"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={() => setOpenFeedbackForm(true)}
ref={feedbackRef}
color="inherit"
style={{float: "right", }}
disableFocusRipple={true}
disableRipple={true}>
<MythicStyledTooltip title="Send Support Feedback">
<ThumbDownTwoTone fontSize={"large"} className="mythicElement" />
</MythicStyledTooltip>
</IconButton>
{openFeedbackForm &&
<MythicDialog fullWidth={true} maxWidth="md" open={openFeedbackForm}
onClose={()=>{setOpenFeedbackForm(false);}}
innerDialog={<MythicFeedbackDialog
title={"Submit Feedback via Webhook"}
onClose={()=>{setOpenFeedbackForm(false);}} />}
/>
}
<IconButton
aria-label="documentation links"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={handleDocumentationMenu}
ref={documentationRef}
color="inherit"
style={{float:"right"}}
disableFocusRipple={true}
disableRipple={true}>
<MythicStyledTooltip title="Help">
<HelpIcon fontSize={"large"} className="mythicElement"/>
</MythicStyledTooltip>
</IconButton>
<Menu
id="menu-appbar"
anchorEl={documentationAnchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
open={Boolean(documentationAnchorEl)}
onClose={handleDocumentationClose}
MenuListProps={{style: {backgroundColor: theme.palette.primary.main, color: "white"}}}
>
<MenuItem component={Link} target="_blank" to="/docs/agents" onClick={handleDocumentationClose}>Agent Documentation</MenuItem>
<MenuItem component={Link} target="_blank" to="/docs/wrappers" onClick={handleDocumentationClose}>Wrapper Documentation</MenuItem>
<MenuItem component={Link} target="_blank" to="/docs/c2-profiles" onClick={handleDocumentationClose}>C2 Profile Documentation</MenuItem>
<MenuItem component={Link} to={{pathname: "https://docs.mythic-c2.net"}} target="_blank" onClick={handleDocumentationClose}>Mythic Documentation</MenuItem>
</Menu>
<TopAppBarNotifications />
<IconButton
aria-label="account of current user"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={handleMenu}
ref={settingsRef}
color="inherit"
style={{float: "right", }}
disableFocusRipple={true}
disableRipple={true} >
<MythicStyledTooltip title="Settings or Logout">
<ManageAccountsIcon fontSize={"large"} className="mythicElement" />
</MythicStyledTooltip>
</IconButton>
{props.theme === 'dark' &&
<IconButton
disableFocusRipple={true}
disableRipple={true}
onClick={props.toggleTheme}
style={{float: "right", paddingRight: "0px", color: '#2f0e67'}}
>
<MythicStyledTooltip title="Change to Light Mode">
<DarkModeTwoToneIcon fontSize={"large"} className="mythicElement" />
</MythicStyledTooltip>
</IconButton>
}
{props.theme === 'light' &&
<IconButton
disableFocusRipple={true}
disableRipple={true}
onClick={props.toggleTheme}
style={{float: "right", paddingRight: "0px", color: '#eacc1b'}}
>
<MythicStyledTooltip title="Change to Dark Mode">
<LightModeTwoToneIcon fontSize={"large"} className="mythicElement" />
</MythicStyledTooltip>
</IconButton>
}
</div>
</div>
</Toolbar>
) : null
}
</StyledAppBar>
<StyledDrawer
className={classes.drawer}
anchor="left"
open={isOpen}
classes={{
paper: classes.drawerPaper,
}}
onClose={handleDrawerClose}
>
<div className={classes.drawerHeader} role="presentation">
<ReactLogo style={{width: "80%", display: "inline-block"}}/>
<IconButton onClick={handleDrawerClose} size="large">
{theme.direction === 'ltr' ? <ChevronLeftIcon className="mythicElement"/> : <ChevronRightIcon className="mythicElement"/>}
</IconButton>
</div>
<Divider />
<List
subheader={
<ListSubheader className={classes.listSubHeader} component="div" id="nested-list-subheader">
Home
</ListSubheader>
}>
<div style={{marginLeft: "15px"}}>
<b>Mythic Version:</b> v{mythicVersion}<br/>
<b>UI Version:</b> v{mythicUIVersion}<br/>
</div>
<ListItem button component={Link} to='/new' key={"home"} onClick={handleDrawerClose}>
<ListItemIcon ><SpaceDashboardTwoToneIcon fontSize={"large"} className="mythicElement" /></ListItemIcon>
<ListItemText primary={"Dashboard"} />
</ListItem>
</List>
<Divider />
<List
subheader={
<ListSubheader className={classes.listSubHeader} component="div" id="nested-list-subheader">
Global Configurations
</ListSubheader>
}>
<ListItem button onClick={handleToggleGlobal}>
<ListItemIcon><LayersTwoToneIcon fontSize={"large"} /></ListItemIcon>
<ListItemText>Services</ListItemText>
{openGlobal ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={openGlobal} unmountOnExit>
<List component="div" disablePadding>
<ListItem button className={classes.nested} target="_blank" component={Link} to='/jupyter' key={"jupyter"} onClick={handleDrawerClose}>
<ListItemIcon><CodeIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Jupyter Notebooks"} />
</ListItem>
<ListItem button className={classes.nested} target="_blank" component={Link} to='/console' key={"console"} onClick={handleDrawerClose}>
<ListItemIcon><StorageIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"GraphQL Console"} />
</ListItem>
<ListItem button className={classes.nested} component={Link} to='/new/consuming_services' key={"consuming"} onClick={handleDrawerClose}>
<ListItemIcon><PublicIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Consuming Services"} />
</ListItem>
</List>
</Collapse>
<ListItem button onClick={handleToggleCreate}>
<ListItemIcon><PostAddIcon /></ListItemIcon>
<ListItemText>Create</ListItemText>
{openCreate ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={openCreate} unmountOnExit>
<List component="div" disablePadding>
<ListItem button className={classes.nested} component={Link} to='/new/createpayload' key={"createpayload"} onClick={handleDrawerClose} state={{from: 'TopAppBar'}}>
<ListItemIcon><PostAddIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Create Payload"} />
</ListItem>
<ListItem button className={classes.nested} component={Link} to='/new/createwrapper' key={"createwrapper"} onClick={handleDrawerClose}>
<ListItemIcon><PostAddIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Create Wrapper"} />
</ListItem>
</List>
</Collapse>
<ListItem button onClick={handleToggleOperations}>
<ListItemIcon><SupervisorAccountIcon fontSize={"large"} /></ListItemIcon>
<ListItemText>Operation Config</ListItemText>
{openOperations ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={openOperations} unmountOnExit>
<List component="div" disablePadding>
<ListItem button className={classes.nested} component={Link} to='/new/payloadtypes' key={"payloadtypes"} onClick={handleDrawerClose}>
<ListItemIcon><HeadsetTwoToneIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Agents & C2"} />
</ListItem>
<ListItem button className={classes.nested} component={Link} to='/new/operations' key={"modifyoperations"} onClick={handleDrawerClose}>
<ListItemIcon><EditIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Modify Operations"} />
</ListItem>
<ListItem button className={classes.nested} component={Link} to='/new/browserscripts' key={"browserscripts"} onClick={handleDrawerClose}>
<ListItemIcon><CodeIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"BrowserScripts"} />
</ListItem>
</List>
</Collapse>
</List>
<Divider />
<List
subheader={
<ListSubheader className={classes.listSubHeader} component="div" id="nested-list-subheader">
Operational Views
</ListSubheader>
}>
<ListItem button onClick={handleToggleData}>
<ListItemIcon><BarChartIcon fontSize={"large"} /></ListItemIcon>
<ListItemText>Operational Data</ListItemText>
{openData ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={openData} unmountOnExit>
<List component="div" disablePadding>
<ListItem button className={classes.nested} component={Link} to='/new/payloads' key={"payloads"} onClick={handleDrawerClose}>
<ListItemIcon><FontAwesomeIcon icon={faBiohazard} size="2x"/></ListItemIcon>
<ListItemText primary={"Payloads"} />
</ListItem>
<ListItem button className={classes.nested} component={Link} to='/new/search?tab=callbacks&searchField=Host&search=' key={"search"} onClick={handleDrawerClose}>
<ListItemIcon><SearchIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Search"} />
</ListItem>
</List>
</Collapse>
<ListItem button component={Link} to='/new/callbacks' key={"callbacks"} onClick={handleDrawerClose}>
<ListItemIcon><PhoneCallbackIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Active Callbacks"} />
</ListItem>
</List>
<Divider />
</StyledDrawer>
</>
);
}
@@ -0,0 +1,72 @@
import React from 'react';
import {useSubscription, gql } from '@apollo/client';
import Badge from '@mui/material/Badge';
import NotificationsActiveTwoToneIcon from '@mui/icons-material/NotificationsActiveTwoTone';
import { useTheme } from '@mui/material/styles';
import {alertCount} from "../cache";
import {MeContext} from "./App";
import {requestCurrentOperationSync} from "./utilities/MythicConnection";
const SUB_Event_Logs = gql`
subscription OperationAlertCounts($operation_id: Int!, $operator_id: Int!){
operation_by_pk(id: $operation_id) {
id
alert_count
operators(where: {id: {_eq: $operator_id}}) {
id
}
}
}
`;
export function TopAppBarVerticalEventLogNotifications() {
const theme = useTheme();
const me = React.useContext(MeContext);
const [alerts, setAlerts] = React.useState(alertCount());
const syncRequestedForOperation = React.useRef(null);
React.useEffect(() => {
syncRequestedForOperation.current = null;
}, [me?.user?.current_operation_id]);
const {error} = useSubscription(SUB_Event_Logs, {
variables: {
operation_id: me?.user?.current_operation_id || 0,
operator_id: me?.user?.user_id || 0,
},
skip: !me?.user?.current_operation_id || !me?.user?.user_id,
onError: data => {
console.log("vertical event log error")
console.error(data);
},
onData: ({data}) => {
const operation = data.data?.operation_by_pk;
if(!operation){
return;
}
if(operation.operators.length === 0 &&
syncRequestedForOperation.current !== operation.id){
syncRequestedForOperation.current = operation.id;
requestCurrentOperationSync();
return;
}
const newAlertCount = operation.alert_count;
if(newAlertCount !== alerts){
setAlerts(newAlertCount);
alertCount(newAlertCount);
}
}
});
return (
error ? (
<Badge color="secondary" badgeContent={"X"}>
<NotificationsActiveTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} />
</Badge>
) : (
<Badge badgeContent={alerts}
color="error" max={99}
>
<NotificationsActiveTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"}/>
</Badge>
)
);
}
@@ -1,54 +0,0 @@
import React from 'react';
import {useSubscription, gql } from '@apollo/client';
import Badge from '@mui/material/Badge';
import NotificationsActiveTwoToneIcon from '@mui/icons-material/NotificationsActiveTwoTone';
import { Link } from 'react-router-dom';
import { IconButton } from '@mui/material';
import {snackActions} from './utilities/Snackbar';
import { meState } from '../cache';
import { useReactiveVar } from '@apollo/client';
import {MythicStyledTooltip} from "./MythicComponents/MythicStyledTooltip";
const SUB_Event_Logs = gql`
subscription MySubscription($operation_id: Int!) {
operation_by_pk(id: $operation_id) {
id
alert_count
}
}
`;
export function TopAppBarNotifications(props) {
const me = useReactiveVar(meState);
const { loading, error, data } = useSubscription(SUB_Event_Logs, {
variables: {operation_id: me?.user?.current_operation_id || 0},
onError: data => {
snackActions.error("Mythic encountered an error getting event log messages: " + data.toString());
console.error(data);
}
});
return (
<MythicStyledTooltip title="Event Feed">
<IconButton
color="inherit"
component={Link}
disableFocusRipple={true}
disableRipple={true}
to='/new/EventFeed'
style={{float: "right"}}>
{error ? (
<Badge color="secondary" badgeContent={0}>
<NotificationsActiveTwoToneIcon fontSize={"large"} />
</Badge>
) : (
<Badge badgeContent={data?.operation_by_pk?.alert_count || 0} color="error">
<NotificationsActiveTwoToneIcon fontSize={"large"} />
</Badge>
)}
</IconButton>
</MythicStyledTooltip>
);
}
File diff suppressed because it is too large. Load diff
@@ -3,7 +3,8 @@ import { gql, useMutation, useSubscription } from '@apollo/client';
import {BrowserScriptsTable} from './BrowserScriptsTable';
import {snackActions} from '../../utilities/Snackbar';
import { Backdrop } from '@mui/material';
import {CircularProgress} from '@mui/material';
import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
import {MythicLoadingState} from "../../MythicComponents/MythicStateDisplay";
const SUB_BrowserScripts = gql`
@@ -78,7 +79,7 @@ export function BrowserScripts({me}){
}
} )
setBrowserScripts(scripts);
if(backdropOpen){setBackdropOpen(false);}
setBackdropOpen(false);
}
});
const [toggleActive] = useMutation(updateBrowserScriptActive, {
@@ -117,6 +118,7 @@ export function BrowserScripts({me}){
}
});
const onToggleActive = ({browserscript_id, active}) => {
setBackdropOpen(true);
toggleActive({variables: {browserscript_id, active}});
}
const onSubmitEdit = ({browserscript_id, script, command_id, payload_type_id}) => {
@@ -134,9 +136,9 @@ export function BrowserScripts({me}){
}
}, [])
return (
<React.Fragment>
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={false}>
<CircularProgress color="inherit" />
<MythicPageBody>
<Backdrop open={backdropOpen} style={{zIndex: 1300, position: "absolute"}} invisible={false}>
<MythicLoadingState compact title="Loading browser scripts" description="Fetching scripts for this operator." sx={{color: "inherit"}} />
</Backdrop>
<BrowserScriptsTable
browserscripts={browserScripts} operation_id={me?.user?.current_operation_id || 0} onToggleActive={onToggleActive}
@@ -144,6 +146,6 @@ export function BrowserScripts({me}){
onSubmitEdit={onSubmitEdit} onRevert={onRevert} onSubmitNew={onSubmitCreateNewBrowserScript}
/>
</React.Fragment>
</MythicPageBody>
);
}
@@ -1,53 +1,130 @@
import React from 'react';
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 Typography from '@mui/material/Typography';
import { BrowserScriptsTableRow } from './BrowserScriptsTableRow';
import {useTheme} from '@mui/material/styles';
import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined';
import Button from '@mui/material/Button';
import AddCircleIcon from '@mui/icons-material/AddCircle';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
import {EditScriptDialog} from './EditScriptDialog';
import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader";
import {
MythicSearchField,
MythicTableToolbar,
MythicTableToolbarGroup,
MythicToolbarButton,
MythicToolbarMenuItem,
MythicToolbarSelect
} from "../../MythicComponents/MythicTableToolbar";
import {MythicTableEmptyState} from "../../MythicComponents/MythicStateDisplay";
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
export function BrowserScriptsTable(props){
const theme = useTheme();
const [openNewScriptDialog, setOpenNewScriptDialog] = React.useState(false);
const [search, setSearch] = React.useState("");
const [statusFilter, setStatusFilter] = React.useState("all");
const normalizedSearch = search.trim().toLowerCase();
const visibleScripts = React.useMemo(() => {
return props.browserscripts.filter((script) => {
const matchesStatus = statusFilter === "all" ||
(statusFilter === "active" && script.active) ||
(statusFilter === "disabled" && !script.active) ||
(statusFilter === "modified" && script.user_modified) ||
(statusFilter === "default" && !script.user_modified);
if(!matchesStatus){
return false;
}
if(normalizedSearch === ""){
return true;
}
return [
script.payloadtype?.name,
script.command?.cmd,
script.author,
].some((value) => (value || "").toLowerCase().includes(normalizedSearch));
});
}, [normalizedSearch, props.browserscripts, statusFilter]);
const scriptCountLabel = props.browserscripts.length === 1 ? "1 script" : `${props.browserscripts.length} scripts`;
const visibleCountLabel = visibleScripts.length === 1 ? "1 shown" : `${visibleScripts.length} shown`;
const activeCount = props.browserscripts.filter((script) => script.active).length;
const activeCountLabel = activeCount === 1 ? "1 active" : `${activeCount} active`;
const modifiedCount = props.browserscripts.filter((script) => script.user_modified).length;
const hasFilter = normalizedSearch !== "" || statusFilter !== "all";
const onChangeSearch = (name, value) => {
setSearch(value);
}
return (
<React.Fragment>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main, marginBottom: "5px", marginTop: "10px", marginRight: "5px"}} variant={"elevation"}>
<Typography variant="h3" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
Browser Scripts
</Typography>
<Button size="small" onClick={() => setOpenNewScriptDialog(true)} style={{float: "right", marginTop: "10px", marginRight: "30px"}} startIcon={<AddCircleOutlineOutlinedIcon/>} color="primary" variant="contained">New Script</Button>
{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
<>
<MythicPageHeader
title={"Browser Scripts"}
subtitle={"Manage custom browser script renderers for task output in the current UI."}
meta={
<>
<MythicPageHeaderChip label={scriptCountLabel} />
{hasFilter && <MythicPageHeaderChip label={visibleCountLabel} />}
<MythicPageHeaderChip label={activeCountLabel} />
{modifiedCount > 0 && <MythicPageHeaderChip label={`${modifiedCount} modified`} />}
</>
}
</Paper>
<TableContainer component={Paper} className="mythicElement" style={{maxHeight: "90%"}}>
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "100%", "overflow": "scroll"}}>
actions={
<MythicToolbarButton variant="contained" colorMode="hover" tone="success" onClick={() => setOpenNewScriptDialog(true)} startIcon={<AddCircleIcon />}>
Script
</MythicToolbarButton>
}
/>
{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} />
} />
}
<MythicTableToolbar variant="search">
<MythicTableToolbarGroup grow>
<MythicSearchField
name="Search browser scripts"
placeholder="Search payload, command, or author..."
value={search}
onChange={onChangeSearch}
/>
</MythicTableToolbarGroup>
<MythicTableToolbarGroup>
<MythicToolbarSelect
value={statusFilter}
onChange={(event) => setStatusFilter(event.target.value)}
>
<MythicToolbarMenuItem value="all">All scripts</MythicToolbarMenuItem>
<MythicToolbarMenuItem value="active">Active</MythicToolbarMenuItem>
<MythicToolbarMenuItem value="disabled">Disabled</MythicToolbarMenuItem>
<MythicToolbarMenuItem value="modified">User modified</MythicToolbarMenuItem>
<MythicToolbarMenuItem value="default">Container default</MythicToolbarMenuItem>
</MythicToolbarSelect>
</MythicTableToolbarGroup>
</MythicTableToolbar>
<TableContainer style={{flexGrow: 1, minHeight: 0, overflow: "auto"}}>
<Table className="mythic-browser-scripts-table table-fixed max-w-full w-full" stickyHeader={true} size="small">
<TableHead>
<TableRow>
<TableCell style={{width: "3rem"}}>Edit</TableCell>
<TableCell style={{width: "5rem"}}>Active</TableCell>
<TableCell style={{width: "15rem"}}>Payload</TableCell>
<TableCell style={{width: "20rem"}}>Command</TableCell>
<TableCell style={{width: "12rem"}}> Author</TableCell>
<TableCell style={{textAlign: "left"}}>User Modified?</TableCell>
<MythicStyledTableCell style={{width: "24rem"}}>Script</MythicStyledTableCell>
<MythicStyledTableCell style={{width: "10rem"}}>Author</MythicStyledTableCell>
<MythicStyledTableCell style={{width: "9.5rem"}}>Active</MythicStyledTableCell>
<MythicStyledTableCell style={{width: "12rem"}}>Source</MythicStyledTableCell>
<MythicStyledTableCell style={{width: "4rem", textAlign: "center"}}>Actions</MythicStyledTableCell>
<MythicStyledTableCell className="mythic-browser-script-spacer-cell min-w-0" />
</TableRow>
</TableHead>
<TableBody>
{props.browserscripts.map( (op) => (
{visibleScripts.length === 0 &&
<MythicTableEmptyState
colSpan={6}
compact
title={props.browserscripts.length === 0 ? "No browser scripts" : "No browser scripts match"}
description={props.browserscripts.length === 0 ? "Create or import browser scripts to customize task output rendering." : "Try changing the search text or status filter."}
/>
}
{visibleScripts.map( (op) => (
<BrowserScriptsTableRow
me={props.me}
operation_id={props.operation_id} onToggleActive={props.onToggleActive}
@@ -58,9 +135,8 @@ export function BrowserScriptsTable(props){
))}
</TableBody>
</Table>
</TableContainer>
</React.Fragment>
</TableContainer>
</>
)
}
@@ -1,11 +1,15 @@
import {MythicActionButton} from "../../MythicComponents/MythicActionButton";
import React from 'react';
import { Switch} from '@mui/material';
import TableCell from '@mui/material/TableCell';
import Box from '@mui/material/Box';
import {Switch} from '@mui/material';
import TableRow from '@mui/material/TableRow';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
import {EditScriptDialog} from './EditScriptDialog';
import SettingsIcon from '@mui/icons-material/Settings';
import IconButton from '@mui/material/IconButton';
import EditIcon from '@mui/icons-material/Edit';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
import {MythicChip} from "../../MythicComponents/MythicChip";
export function BrowserScriptsTableRow(props){
const [openEdit, setOpenEdit] = React.useState(false);
@@ -20,23 +24,44 @@ export function BrowserScriptsTableRow(props){
}
return (
<React.Fragment>
<TableRow key={"payload" + props.id} hover>
<TableCell >
<IconButton size="small" onClick={()=>{setOpenEdit(true);}} color="info" variant="contained"><SettingsIcon color="info" /></IconButton>
</TableCell>
<TableCell >
<Switch
checked={props.active}
onChange={onToggleActive}
color="primary"
inputProps={{ 'aria-label': 'checkbox', "track": "white" }}
name="Active"
/>
</TableCell>
<TableCell>{props.payloadtype.name}</TableCell>
<TableCell>{props.command.cmd}</TableCell>
<TableCell>{props.author}</TableCell>
<TableCell>{props.user_modified ? "User Modified" : "" } </TableCell>
<TableRow className={props.active ? "" : "mythic-browser-script-row-disabled"} key={"payload" + props.id} hover>
<MythicStyledTableCell>
<Box className="mythic-browser-script-script-cell items-center flex gap-5 min-w-0">
<MythicStyledTooltip title={props.payloadtype.name}>
<Box className="mythic-browser-script-payload-icon items-center inline-flex flex-none justify-center rounded bg-neutral-1 border-subtle">
<MythicAgentSVGIcon payload_type={props.payloadtype.name} style={{width: "32px", height: "32px"}} />
</Box>
</MythicStyledTooltip>
<Box className="mythic-browser-script-script-copy flex flex-column gap-1 min-w-0">
<Box className="mythic-browser-script-command-name text-sm font-750 leading-125 truncate text-primary whitespace-nowrap">{props.command.cmd}</Box>
<Box className="mythic-browser-script-payload-name text-xs font-650 leading-125 truncate text-muted whitespace-nowrap">{props.payloadtype.name}</Box>
</Box>
</Box>
</MythicStyledTableCell>
<MythicStyledTableCell>{props.author}</MythicStyledTableCell>
<MythicStyledTableCell>
<Box className="mythic-browser-script-active-cell items-center flex gap-3 min-w-0">
<Switch
checked={props.active}
onChange={onToggleActive}
color="success"
inputProps={{ 'aria-label': 'Toggle browser script active state', "track": "white" }}
name="Active"
size="small"
/>
</Box>
</MythicStyledTableCell>
<MythicStyledTableCell>
<MythicChip label={props.user_modified ? "User modified" : "Container default"} tone={props.user_modified ? "warning" : "secondary"} />
</MythicStyledTableCell>
<MythicStyledTableCell style={{textAlign: "center"}}>
<Box className="items-center flex flex-wrap gap-3 flex-nowrap mythic-browser-script-actions justify-center">
<MythicActionButton iconOnly appearance="raised" colorMode="hover" tone="info" size="small" onClick={()=>{setOpenEdit(true);}}>
<EditIcon fontSize="small" />
</MythicActionButton>
</Box>
</MythicStyledTableCell>
<MythicStyledTableCell className="mythic-browser-script-spacer-cell min-w-0" />
{openEdit &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openEdit}
@@ -50,4 +75,3 @@ export function BrowserScriptsTableRow(props){
</React.Fragment>
)
}
@@ -1,6 +1,7 @@
import {MythicActionButton} from "../../MythicComponents/MythicActionButton";
import React, {useEffect, useRef} from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import Box from '@mui/material/Box';
import Collapse from '@mui/material/Collapse';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import AceEditor from 'react-ace';
@@ -14,11 +15,22 @@ import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl';
import Select from '@mui/material/Select';
import InputLabel from '@mui/material/InputLabel';
import Input from '@mui/material/Input';
import Tooltip from '@mui/material/Tooltip';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import Split from 'react-split';
import {TaskDisplay} from "../Callbacks/TaskDisplay";
import {MythicChip} from "../../MythicComponents/MythicChip";
import {taskingDataFragment} from '../Callbacks/CallbackMutations'
import {
MythicDialogBody,
MythicDialogButton,
MythicDialogFooter,
MythicFormField,
MythicFormGrid,
MythicFormNote
} from "../../MythicComponents/MythicDialogLayout";
import {MythicEmptyState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay";
const getCommandsAndPayloadTypesQuery = gql`
@@ -42,6 +54,38 @@ query getAvailableTasks($command_id: Int!){
}
`;
const BROWSER_SCRIPT_WORKBENCH_SPLIT_KEY = "browserScriptWorkbenchSplitSizes";
const BROWSER_SCRIPT_TOP_SPLIT_KEY = "browserScriptTopSplitSizes";
const defaultWorkbenchSplitSizes = [48, 52];
const defaultTopSplitSizes = [68, 32];
const getStoredSplitSizes = (storageKey, fallback) => {
try {
if(typeof window === "undefined"){
return fallback;
}
const storedSizes = window.localStorage.getItem(storageKey);
if(!storedSizes){
return fallback;
}
const parsedSizes = JSON.parse(storedSizes);
if(!Array.isArray(parsedSizes) || parsedSizes.length !== fallback.length || !parsedSizes.every(Number.isFinite)){
return fallback;
}
return parsedSizes;
} catch (error) {
return fallback;
}
}
const storeSplitSizes = (storageKey, sizes) => {
try {
window.localStorage.setItem(storageKey, JSON.stringify(sizes));
} catch (error) {
// Best effort only; layout still works without persisted sizes.
}
}
export function EditScriptDialog(props) {
const theme = useTheme();
const [script, setScript] = React.useState("");
@@ -49,12 +93,19 @@ export function EditScriptDialog(props) {
const [selectedCommand, setSelectedCommand] = React.useState('');
const [payloadTypeCmdOptions, setPayloadTypeCmdOptions] = React.useState([]);
const [commandOptions, setCommandOptions] = React.useState([]);
const inputPTRef = useRef(null);
const inputCMDRef = useRef(null);
const inputTaskRef = useRef(null);
const [availableTasks, setAvailableTasks] = React.useState([]);
const [selectedTask, setSelectedTask] = React.useState('');
useQuery(getCommandsAndPayloadTypesQuery, {
const [workbenchSplitSizes, setWorkbenchSplitSizes] = React.useState(() => getStoredSplitSizes(BROWSER_SCRIPT_WORKBENCH_SPLIT_KEY, defaultWorkbenchSplitSizes));
const [topSplitSizes, setTopSplitSizes] = React.useState(() => getStoredSplitSizes(BROWSER_SCRIPT_TOP_SPLIT_KEY, defaultTopSplitSizes));
const [targetOpen, setTargetOpen] = React.useState(() => props.new === true);
const selectedPayloadTypeOption = React.useMemo(() => {
return payloadTypeCmdOptions.find((payloadType) => payloadType.id === selectedPayloadType);
}, [payloadTypeCmdOptions, selectedPayloadType]);
const selectedCommandOption = React.useMemo(() => {
return commandOptions.find((command) => command.id === selectedCommand);
}, [commandOptions, selectedCommand]);
const taskSelectorValue = selectedTask || "";
const {loading: targetLoading} = useQuery(getCommandsAndPayloadTypesQuery, {
onCompleted: data => {
setPayloadTypeCmdOptions(data.payloadtype);
if(props.payload_type_id !== undefined){
@@ -66,11 +117,8 @@ export function EditScriptDialog(props) {
}
if(props.command_id !== undefined){
setSelectedCommand(props.command_id);
for(let i = 0; i < data.payloadtype.length; i++){
if(props.payload_type_id === data.payloadtype[i].id){
setCommandOptions(data.payloadtype[i].commands)
}
}
const payloadType = data.payloadtype.find((payloadType) => payloadType.id === props.payload_type_id);
setCommandOptions(payloadType?.commands || []);
}else{
if(data.payloadtype.length > 0){
if(data.payloadtype[0].commands.length > 0){
@@ -84,11 +132,13 @@ export function EditScriptDialog(props) {
}
});
const [getAvailableTasks] = useLazyQuery(getExistingTasksForCommand, {
const [getAvailableTasks, {loading: tasksLoading}] = useLazyQuery(getExistingTasksForCommand, {
onCompleted: data => {
setAvailableTasks(data.task);
if(data.task.length > 0){
setSelectedTask(data.task[0]);
}else{
setSelectedTask('');
}
},
onError: data => {
@@ -102,16 +152,18 @@ export function EditScriptDialog(props) {
useEffect( () => {
if(selectedCommand !== ""){
getAvailableTasks({variables: {command_id: selectedCommand}})
}else{
setAvailableTasks([]);
setSelectedTask('');
}
}, [selectedCommand]);
}, [selectedCommand, getAvailableTasks]);
useEffect( () => {
if(props.script !== undefined){
try{
//setScript(atob(props.script));
setScript(decodeURIComponent(window.atob(props.script)));
}catch(error){
console.log(error)
setScript(props.script);
}
}
@@ -138,9 +190,12 @@ export function EditScriptDialog(props) {
}
const onChangeSelectedPayloadType = (event) => {
setSelectedPayloadType(event.target.value);
const cmds = payloadTypeCmdOptions.filter( (p) => p.id === event.target.value);
setCommandOptions(cmds[0].commands);
setSelectedCommand(cmds[0].commands[0].id);
const selectedPayload = payloadTypeCmdOptions.find( (p) => p.id === event.target.value);
const cmds = selectedPayload?.commands || [];
setCommandOptions(cmds);
setSelectedCommand(cmds[0]?.id || '');
setAvailableTasks([]);
setSelectedTask('');
}
const onChangeTask = (event) => {
setSelectedTask(event.target.value);
@@ -149,34 +204,48 @@ export function EditScriptDialog(props) {
setSelectedCommand(event.target.value);
}
const onLoad = (editor) => {
// Your editor options comes here
editor.on('change', (arg, activeEditor) => {
editorRef.current = activeEditor;
editor.removeEventListener('change');
editorRef.current.resize();
});
editorRef.current = editor;
requestAnimationFrame(() => editor.resize());
}
const onOutputLoad = (editor) => {
// Your editor options comes here
editor.on('change', (arg, activeEditor) => {
outputRef.current = activeEditor;
editor.removeEventListener('change');
outputRef.current.resize();
});
outputRef.current = editor;
requestAnimationFrame(() => editor.resize());
}
React.useEffect( () => {
var logBackup = console.log;
console.log = function(msg) {
logStreamRef.current += "\n" + msg;
logBackup.apply(msg);
setLogOutput(logStreamRef.current)
const logBackup = console.log;
const formatConsoleArg = (arg) => {
if(typeof arg === "string"){
return arg;
}
try {
return JSON.stringify(arg, null, 2);
} catch (error) {
return String(arg);
}
};
console.log = function(...args) {
const message = args.map(formatConsoleArg).join(" ");
logStreamRef.current += "\n" + message;
setLogOutput(logStreamRef.current);
logBackup.apply(console, args);
};
return () => {
console.log = logBackup;
};
}, []);
const onDragging = () => {
React.useEffect(() => {
requestAnimationFrame(() => {
if(editorRef.current){
editorRef.current.resize();
}
if(outputRef.current){
outputRef.current.resize();
}
});
}, [selectedTask, tasksLoading]);
const resizeEditors = () => {
if(editorRef.current){
editorRef.current.resize();
}
@@ -184,128 +253,227 @@ export function EditScriptDialog(props) {
outputRef.current.resize();
}
}
const onWorkbenchDragEnd = (sizes) => {
setWorkbenchSplitSizes(sizes);
storeSplitSizes(BROWSER_SCRIPT_WORKBENCH_SPLIT_KEY, sizes);
resizeEditors();
}
const onTopDragEnd = (sizes) => {
setTopSplitSizes(sizes);
storeSplitSizes(BROWSER_SCRIPT_TOP_SPLIT_KEY, sizes);
resizeEditors();
}
const onToggleTargetOpen = () => {
setTargetOpen((current) => !current);
}
const selectedTaskLabel = (task) => {
return `${task.command_name} / ${task.display_id} / ${task.display_params}`;
}
return (
<React.Fragment>
<DialogTitle >
{props.title ? props.title : "Edit " + props.author + "'s BrowserScript Code"}
</DialogTitle>
<DialogContent dividers={true} style={{height: `calc(100vh)`, display: "flex", flexDirection: "column", width: "100%"}}>
<div style={{display: "flex"}}>
<FormControl style={{width: "50%"}}>
<InputLabel ref={inputPTRef}>Payload Type</InputLabel>
<Select
labelId="demo-dialog-select-label"
id="demo-dialog-select"
value={selectedPayloadType}
onChange={onChangeSelectedPayloadType}
input={<Input style={{width: "100%"}}/>}
<DialogTitle>
<Box className="mythic-dialog-title-row items-center flex flex-wrap gap-5 justify-between min-w-0">
<Box component="span">{props.title ? props.title : "Edit " + props.author + "'s BrowserScript Code"}</Box>
</Box>
</DialogTitle>
<DialogContent className="mythic-browser-script-dialog-content flex flex-column overflow-hidden" dividers={true}>
<MythicDialogBody className="mythic-browser-script-dialog-body flex-fill min-h-0 overflow-hidden">
<Box className={`mythic-browser-script-target-panel rounded bg-surface-raised border-subtle shadow-1${targetOpen ? " mythic-browser-script-target-panel-open" : ""} overflow-hidden`}>
<Box className="mythic-browser-script-target-summary items-center flex gap-5 justify-between">
<Box className="mythic-browser-script-target-copy items-center flex gap-5 min-w-0">
<Box component="span" className="mythic-browser-script-target-label text-xs font-800 leading-100 flex-none text-muted">Script Target</Box>
<Box className="mythic-browser-script-target-chips items-center flex flex-wrap gap-3 min-w-0">
<MythicChip size="small" label={selectedPayloadTypeOption?.name || (targetLoading ? "Loading payload types" : "No payload type")} />
<MythicChip size="small" label={selectedCommandOption?.cmd || "No command"} />
</Box>
</Box>
<Tooltip title={targetOpen ? "Collapse target settings" : "Edit target settings"}>
<MythicActionButton iconOnly
aria-expanded={targetOpen}
aria-label={targetOpen ? "Collapse target settings" : "Edit target settings"}
appearance="raised" colorMode="hover" tone="primary"
onClick={onToggleTargetOpen}
size="small"
>
{targetOpen ? <KeyboardArrowUpIcon fontSize="small" /> : <KeyboardArrowDownIcon fontSize="small" />}
</MythicActionButton>
</Tooltip>
</Box>
<Collapse
in={targetOpen}
onEntered={resizeEditors}
onExited={resizeEditors}
timeout="auto"
unmountOnExit
>
{payloadTypeCmdOptions.map( (opt) => (
<MenuItem value={opt.id} key={"payloadtype" + opt.id}>{opt.name}</MenuItem>
) )}
</Select>
</FormControl>
<FormControl style={{width: "50%", paddingBottom: "10px"}}>
<InputLabel ref={inputCMDRef}>Command</InputLabel>
<Select
labelId="demo-dialog-select-label"
id="demo-dialog-select"
value={selectedCommand}
onChange={onChangeSelectedCommand}
input={<Input style={{width: "100%"}}/>}
<Box className="mythic-browser-script-target-details p-5 flex flex-column gap-4 bg-surface-muted">
<MythicFormGrid minWidth="16rem">
<MythicFormField label="Payload Type">
<FormControl fullWidth size="small">
<InputLabel id="browser-script-payload-type-label">Payload Type</InputLabel>
<Select
label="Payload Type"
labelId="browser-script-payload-type-label"
value={selectedPayloadType}
onChange={onChangeSelectedPayloadType}
disabled={targetLoading}
>
{payloadTypeCmdOptions.map( (opt) => (
<MenuItem value={opt.id} key={"payloadtype" + opt.id}>{opt.name}</MenuItem>
) )}
</Select>
</FormControl>
</MythicFormField>
<MythicFormField label="Command">
<FormControl fullWidth size="small">
<InputLabel id="browser-script-command-label">Command</InputLabel>
<Select
label="Command"
labelId="browser-script-command-label"
value={selectedCommand}
onChange={onChangeSelectedCommand}
disabled={targetLoading || commandOptions.length === 0}
>
{commandOptions.map( (opt) => (
<MenuItem value={opt.id} key={"command" + opt.id}>{opt.cmd}</MenuItem>
) )}
</Select>
</FormControl>
</MythicFormField>
</MythicFormGrid>
<MythicFormNote>
Use Save for Testing to update the script without closing the editor. Select an already executed matching task below, then collapse and re-expand that task if you need it to reload the updated renderer.
</MythicFormNote>
</Box>
</Collapse>
</Box>
<Split
className="mythic-browser-script-workbench flex flex-column flex-fill min-h-0 min-w-0 w-full"
direction="vertical"
gutterSize={8}
minSize={[210, 300]}
onDrag={resizeEditors}
onDragEnd={onWorkbenchDragEnd}
sizes={workbenchSplitSizes}
>
<Split
className="mythic-browser-script-top-split flex min-h-0 min-w-0 w-full"
gutterSize={8}
minSize={[320, 220]}
onDrag={resizeEditors}
onDragEnd={onTopDragEnd}
sizes={topSplitSizes}
>
{commandOptions.map( (opt) => (
<MenuItem value={opt.id} key={"command" + opt.id}>{opt.cmd}</MenuItem>
) )}
</Select>
</FormControl>
</div>
<p>
<b>To test locally</b>: Make your changes in the top left code box. Any <b>console.log</b> entries will appear in the box to the right when executed.
Click <b>Save for Testing</b> to finalize your changes. Make sure to select an already executed task that matches this command from the bottom.
You will need to collapse and re-expand the task to pull in your updated changes.
</p>
<div style={{display: "flex", width: "100%", flexGrow: 1}}>
<Split direction="vertical" style={{height: "100%", width: "100%"}} sizes={[30, 70]} onDrag={onDragging} >
<Split style={{display: "flex"}} sizes={[70, 30]} onDrag={onDragging} >
<div className="bg-gray-light">
<AceEditor
mode="javascript"
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
width="100%"
onLoad={onLoad}
height="100%"
value={script}
focus={true}
onChange={onChange}
setOptions={{
useWorker: false
}}
/>
<div className="mythic-browser-script-pane flex flex-column mythic-browser-script-editor-pane h-full min-h-0 min-w-0 overflow-hidden rounded bg-surface-raised border-subtle">
<div className="mythic-browser-script-pane-header text-xs font-800 items-center flex flex-none justify-between bg-surface-muted border-b-subtle text-primary">
<span>Script Code</span>
<span>JavaScript</span>
</div>
<div className="mythic-browser-script-editor-frame flex-fill min-h-0 min-w-0">
<AceEditor
mode="javascript"
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
width="100%"
onLoad={onLoad}
height="100%"
value={script}
focus={true}
onChange={onChange}
setOptions={{
displayIndentGuides: true,
fontSize: 13,
showPrintMargin: false,
tabSize: 4,
useWorker: false,
}}
/>
</div>
</div>
<div className="bg-gray-light" >
<AceEditor
mode="javascript"
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
width="100%"
onLoad={onOutputLoad}
height="100%"
value={logOutput}
focus={false}
onChange={onChange}
setOptions={{
useWorker: false
}}
/>
<div className="mythic-browser-script-pane flex flex-column mythic-browser-script-console-pane h-full min-h-0 min-w-0 overflow-hidden rounded bg-surface-raised border-subtle">
<div className="mythic-browser-script-pane-header text-xs font-800 items-center flex flex-none justify-between bg-surface-muted border-b-subtle text-primary">
<span>Console Output</span>
<span>console.log</span>
</div>
<div className="mythic-browser-script-editor-frame flex-fill min-h-0 min-w-0">
<AceEditor
mode="javascript"
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
width="100%"
onLoad={onOutputLoad}
height="100%"
value={logOutput}
focus={false}
readOnly
setOptions={{
fontSize: 12,
highlightActiveLine: false,
showGutter: false,
showPrintMargin: false,
tabSize: 4,
useWorker: false,
}}
/>
</div>
</div>
</Split>
<div className="bg-gray-light" >
<FormControl style={{width: "100%"}}>
<InputLabel ref={inputTaskRef} style={{paddingTop: "10px"}}>Test Script With Task</InputLabel>
<Select
labelId="demo-dialog-select-label"
id="demo-dialog-select"
value={selectedTask}
onChange={onChangeTask}
input={<Input style={{width: "100%"}}/>}
>
{availableTasks.map( (opt) => (
<MenuItem value={opt} key={"task" + opt.id}>{opt.command_name + " " + opt.display_params}</MenuItem>
) )}
</Select>
</FormControl>
{selectedTask !== "" &&
<TaskDisplay me={props.me} task={selectedTask} command_id={selectedTask.command == null ? 0 : selectedTask.command.id} />
}
<div className="mythic-browser-script-pane flex flex-column mythic-browser-script-preview-pane h-full min-h-0 min-w-0 overflow-hidden rounded bg-surface-raised border-subtle">
<div className="mythic-browser-script-pane-header text-xs font-800 items-center flex flex-none justify-between bg-surface-muted border-b-subtle text-primary">
<span>Test Preview</span>
<span>{availableTasks.length === 1 ? "1 task" : `${availableTasks.length} tasks`}</span>
</div>
<div className="mythic-browser-script-preview-controls bg-header flex-none border-b-subtle">
<FormControl fullWidth size="small">
<InputLabel id="browser-script-test-task-label">Test Script With Task</InputLabel>
<Select
label="Test Script With Task"
labelId="browser-script-test-task-label"
value={taskSelectorValue}
onChange={onChangeTask}
disabled={tasksLoading || availableTasks.length === 0}
onOpen={resizeEditors}
renderValue={(task) => task ? selectedTaskLabel(task) : ""}
>
{availableTasks.map( (opt) => (
<MenuItem value={opt} key={"task" + opt.id}>{selectedTaskLabel(opt)}</MenuItem>
) )}
</Select>
</FormControl>
</div>
<div className="mythic-browser-script-preview-frame p-4 flex-fill min-h-0 min-w-0 overflow-auto">
{tasksLoading ? (
<MythicLoadingState compact title="Loading test tasks" description="Fetching previous tasks for this command." />
) : selectedTask !== "" ? (
<TaskDisplay me={props.me} task={selectedTask} command_id={selectedTask.command == null ? 0 : selectedTask.command.id} />
) : (
<MythicEmptyState compact title="No matching tasks" description="Run this command at least once to preview this browser script against task output." />
)}
</div>
</div>
</Split>
</div>
</MythicDialogBody>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
</MythicDialogButton>
{props.new ? (
<Button onClick={onSubmit} variant="contained" color="success">
<MythicDialogButton disabled={selectedCommand === "" || selectedPayloadType === ""} intent="primary" onClick={onSubmit}>
Create
</Button>
</MythicDialogButton>
) : (
<React.Fragment>
<Button onClick={onRevert} variant="contained" color="warning">
<MythicDialogButton intent="warning" onClick={onRevert}>
Revert
</Button>
<Button onClick={onTest} variant="contained" color="info">
</MythicDialogButton>
<MythicDialogButton intent="info" disabled={selectedCommand === "" || selectedPayloadType === ""} onClick={onTest}>
Save For Testing
</Button>
<Button onClick={onSubmit} variant="contained" color="success">
</MythicDialogButton>
<MythicDialogButton disabled={selectedCommand === "" || selectedPayloadType === ""} intent="primary" onClick={onSubmit}>
Save and Exit
</Button>
</MythicDialogButton>
</React.Fragment>
)}
</DialogActions>
</MythicDialogFooter>
</React.Fragment>
);
}
@@ -1,6 +1,4 @@
import React, {useEffect} from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import React from 'react';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import List from '@mui/material/List';
@@ -8,10 +6,14 @@ import ListItem from '@mui/material/ListItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import Checkbox from '@mui/material/Checkbox';
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 {MythicActionButton} from '../../MythicComponents/MythicActionButton';
import {
MythicDialogButton,
MythicDialogBody,
MythicDialogFooter,
MythicDialogSection
} from '../../MythicComponents/MythicDialogLayout';
const getCommandsQuery = gql`
query GetCallbackDetails($callback_id: Int!) {
@@ -19,85 +21,129 @@ query GetCallbackDetails($callback_id: Int!) {
loadedcommands {
command {
cmd
payloadtype {
name
}
id
}
id
}
payload {
payloadtype {
commands {
name
commands(where: {deleted: {_eq: false}}) {
cmd
id
}
}
}
}
command(where: {deleted: {_eq: false}, payloadtype: {agent_type: {_eq: "command_augment"}}}){
cmd
id
payloadtype {
name
}
}
}
`;
const CustomListElement = React.memo(({valueObj, handleToggle}) => {
return (
<ListItem style={{padding: 0}} key={valueObj.display_value} role="listitem" button
onClick={() => handleToggle(valueObj)}>
<ListItemIcon>
<Checkbox
checked={valueObj.checked}
disableRipple
inputProps={{'aria-labelledby': valueObj.label}}
/>
</ListItemIcon>
<ListItemText id={valueObj.label} primary={valueObj.display_value}/>
</ListItem>
)
});
const CustomList = ({title, items, handleToggle}) => {
const renderedList = React.useMemo( () => {
return items.map((valueObj) => (
<CustomListElement key={valueObj.display_value} valueObj={valueObj} handleToggle={handleToggle} />
))
}, [items, handleToggle]);
return (
<div className="mythic-transfer-list flex flex-column min-w-0 overflow-hidden w-full rounded bg-surface border-subtle">
<div className="mythic-transfer-list-header bg-table-header text-xs font-700 leading-125 border-b text-primary">{title}</div>
<div className="mythic-transfer-list-body flex-fill overflow-auto">
<List dense component="div" role="list" style={{padding: 0, width: "100%"}}>
{renderedList}
</List>
</div>
</div>
)
};
export function AddRemoveCallbackCommandsDialog(props) {
const [checked, setChecked] = React.useState([]);
const [left, setLeft] = React.useState([]);
const [originalLeft, setOriginalLeft] = React.useState([]);
const [originalRight, setOriginalRight] = React.useState([]);
const [right, setRight] = React.useState([]);
const [leftTitle, setLeftTitle] = React.useState("Commands Not Included");
const [rightTitle, setRightTitle] = React.useState("Commands Included");
const leftChecked = intersection(checked, left);
const rightChecked = intersection(checked, right);
const leftTitle = "Commands Not Included";
const rightTitle = "Commands Included";
useQuery(getCommandsQuery, {variables: {callback_id: props.callback_id},
fetchPolicy: "no-cache",
onCompleted: (data) => {
setOriginalLeft(data.callback_by_pk.payload.payloadtype.commands);
let originalLeftFromQuery = data.callback_by_pk.payload.payloadtype.commands.map(c => {
return {...c, payloadtype: {name: data.callback_by_pk.payload.payloadtype.name}}
})
originalLeftFromQuery = [...originalLeftFromQuery, ...data.command];
setOriginalRight(data.callback_by_pk.loadedcommands);
const leftData = data.callback_by_pk.payload.payloadtype.commands.reduce( (prev, cur) => {
if( data.callback_by_pk.loadedcommands.filter(c => c.command.cmd === cur.cmd).length === 0){
let leftData = originalLeftFromQuery.reduce( (prev, cur) => {
if( data.callback_by_pk.loadedcommands.filter(c => c.command.cmd === cur.cmd && c.command.payloadtype.name === cur.payloadtype.name).length === 0){
return [...prev, cur];
} else {
return [...prev];
}
}, []);
leftData.sort( (a,b) => a.cmd < b.cmd ? -1 : 1);
leftData = leftData.map( c => {return {...c,
checked: false,
display_value: c.cmd + " (" + c?.payloadtype?.name + ")",
label: `transfer-list-item-${c.cmd + " (" + c?.payloadtype?.name + ")"}-label`
}});
setLeft(leftData);
const rightData = data.callback_by_pk.loadedcommands.map( c => c.command);
let rightData = data.callback_by_pk.loadedcommands.map( c => c.command);
rightData.sort( (a,b) => a.cmd < b.cmd ? -1 : 1);
rightData = rightData.map( c => {return {...c,
checked: false,
display_value: c.cmd + " (" + c?.payloadtype?.name + ")",
label: `transfer-list-item-${c.cmd + " (" + c?.payloadtype?.name + ")"}-label`
}});
setRight(rightData);
},
onError: (data) => {
}
})
function not(a, b) {
if(props.itemKey){
return a.filter( (value) => b.find( (element) => element[props.itemKey] === value[props.itemKey] ) === undefined)
}
return a.filter((value) => b.indexOf(value) === -1);
}
function intersection(a, b) {
if(props.itemKey){
return a.filter( (value) => b.find( (element) => element[props.itemKey] === value[props.itemKey] ) !== undefined)
}
return a.filter((value) => b.indexOf(value) !== -1);
}
const handleToggle = (value) => () => {
let currentIndex = -1;
if(props.itemKey){
currentIndex = checked.findIndex( (element) => element[props.itemKey] === value[props.itemKey]);
}else{
currentIndex = checked.indexOf(value);
}
const newChecked = [...checked];
if (currentIndex === -1) {
newChecked.push(value);
} else {
newChecked.splice(currentIndex, 1);
const handleToggle = (value) => {
let found = false;
const newLeft = left.map( c => {
if(c.display_value === value.display_value){
c.checked = !c.checked;
found = true;
}
return c;
});
if(found){
setLeft(newLeft);
return
}
setChecked(newChecked);
const newRight = right.map( c => {
if(c.display_value === value.display_value){
c.checked = !c.checked;
}
return c;
});
setRight(newRight);
};
const handleAllRight = () => {
@@ -106,127 +152,110 @@ export function AddRemoveCallbackCommandsDialog(props) {
};
const handleCheckedRight = () => {
setRight(right.concat(leftChecked));
setLeft(not(left, leftChecked));
setChecked(not(checked, leftChecked));
let leftChecked = left.filter(c => c.checked);
leftChecked = leftChecked.map(c => {return {...c, checked: false}})
let leftNotChecked = left.filter(c => !c.checked);
setRight(right.concat(leftChecked));
setLeft(leftNotChecked);
};
const handleCheckedLeft = () => {
setLeft(left.concat(rightChecked));
setRight(not(right, rightChecked));
setChecked(not(checked, rightChecked));
let rightChecked = right.filter(c => c.checked);
rightChecked = rightChecked.map(c => {return {...c, checked: false}})
let rightNotChecked = right.filter(c => !c.checked);
setLeft(left.concat(rightChecked));
setRight(rightNotChecked);
};
const handleAllLeft = () => {
setLeft(left.concat(right));
setRight([]);
};
const customList = (title, items) => (
<>
<CardHeader className={classes.cardHeader} title={title} />
<StyledDivider classes={{root: classes.divider}}/>
<CardContent style={{flexGrow: 1, overflowY: "auto", padding: 0}}>
<List dense component="div" role="list" style={{padding:0, width: "100%"}}>
{items.map((valueObj) => {
const value = valueObj.cmd;
const labelId = `transfer-list-item-${value}-label`;
return (
<ListItem style={{padding:0}} key={value} role="listitem" button onClick={handleToggle(valueObj)}>
<ListItemIcon>
<Checkbox
checked={checked.findIndex( (element) => element.cmd === value) !== -1}
tabIndex={-1}
disableRipple
inputProps={{ 'aria-labelledby': labelId }}
/>
</ListItemIcon>
<ListItemText id={labelId} primary={value} />
</ListItem>
);
})}
<ListItem />
</List>
</CardContent>
</>
);
const setFinalTags = () => {
// things to add are in the `right` now but weren't for `originalRight`
const commandsToAdd = right.filter( (command) => {
return originalRight.filter(orig => orig.command.cmd == command.cmd).length == 0;
return originalRight.filter(orig => orig.command.cmd === command.cmd && orig.command.payloadtype.name === command.payloadtype.name).length === 0;
});
const commandsToRemove = originalRight.filter( (command) => {
return right.filter(newCommand => newCommand.cmd == command.command.cmd).length == 0;
return right.filter(newCommand => newCommand.cmd === command.command.cmd && newCommand.payloadtype.name === command.command.payloadtype.name).length === 0;
})
props.onSubmit({commandsToAdd, commandsToRemove});
props.onClose();
}
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%"}}>
This will add or remove commands associated with this callback from Mythic's perspective.
This does NOT add or remove commands within the payload itself that's beaconing out to Mythic.
<div style={{display: "flex", flexDirection: "row", overflowY: "auto", flexGrow: 1, minHeight: 0}}>
<div style={{paddingLeft: 0, flexGrow: 1, marginLeft: 0, marginRight: "10px", position: "relative", overflowY: "auto", display: "flex", flexDirection: "column" }}>
{customList(leftTitle, left)}
</div>
<div style={{display: "flex", flexDirection: "column", justifyContent: "center"}}>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleAllRight}
disabled={left.length === 0}
aria-label="move all right"
>
&gt;&gt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleCheckedRight}
disabled={leftChecked.length === 0}
aria-label="move selected right"
>
&gt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleCheckedLeft}
disabled={rightChecked.length === 0}
aria-label="move selected left"
>
&lt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleAllLeft}
disabled={right.length === 0}
aria-label="move all left"
>
&lt;&lt;
</StyledButton>
</div>
<div style={{marginLeft: "10px", position: "relative", flexGrow: 1, display: "flex", flexDirection: "column" }}>
{customList(rightTitle, right)}
</div>
</div>
<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"}}>
<MythicDialogBody>
<MythicDialogSection
title="Command Availability"
description="Updates Mythic's callback command association without changing commands inside the active payload."
>
<div style={{display: "flex", flexDirection: "row", overflowY: "auto", flexGrow: 1, minHeight: 0}}>
<div style={{paddingLeft: 0, flexGrow: 1, marginLeft: 0, marginRight: "10px", position: "relative", overflowY: "auto", display: "flex", flexDirection: "column" }}>
<CustomList title={leftTitle} items={left} handleToggle={handleToggle}/>
</div>
<div className="mythic-transfer-controls py-0 px-4 items-center flex flex-column gap-6 justify-center">
<MythicActionButton
colorMode="always"
tone="primary"
variant="contained"
size="small"
onClick={handleAllRight}
disabled={left.length === 0}
aria-label="move all right"
>
&gt;&gt;
</MythicActionButton>
<MythicActionButton
colorMode="always"
tone="primary"
variant="contained"
size="small"
onClick={handleCheckedRight}
aria-label="move selected right"
>
&gt;
</MythicActionButton>
<MythicActionButton
colorMode="always"
tone="primary"
variant="contained"
size="small"
onClick={handleCheckedLeft}
aria-label="move selected left"
>
&lt;
</MythicActionButton>
<MythicActionButton
colorMode="always"
tone="primary"
variant="contained"
size="small"
onClick={handleAllLeft}
disabled={right.length === 0}
aria-label="move all left"
>
&lt;&lt;
</MythicActionButton>
</div>
<div style={{marginLeft: "10px", position: "relative", flexGrow: 1, display: "flex", flexDirection: "column" }}>
<CustomList title={rightTitle} items={right} handleToggle={handleToggle} />
</div>
</div>
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
<Button onClick={setFinalTags} variant="contained" color="success">
</MythicDialogButton>
<MythicDialogButton intent="primary" onClick={setFinalTags}>
Submit
</Button>
</DialogActions>
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
);
}
@@ -0,0 +1,177 @@
import {createSandboxedWorker} from "./BrowserScriptSandbox";
const DEFAULT_TIMEOUT_MS = 10000;
const toError = (errorData, phase) => {
const error = new Error(errorData?.message || "Browser script failed");
error.name = errorData?.name || "Error";
error.stack = errorData?.stack || error.stack;
error.phase = phase;
return error;
};
export class BrowserScriptRunner {
constructor({workerFactory=createSandboxedWorker, timeoutMS=DEFAULT_TIMEOUT_MS} = {}) {
this.workerFactory = workerFactory;
this.timeoutMS = timeoutMS;
this.nextRequestID = 1;
this.latestRequestID = 0;
this.activeRequest = undefined;
this.queuedRequest = undefined;
this.workerScript = undefined;
this.disposed = false;
}
startWorker = () => {
this.worker = this.workerFactory();
this.worker.onmessage = this.handleMessage;
this.worker.onerror = this.handleWorkerError;
};
restartWorker = () => {
if(this.worker){
this.worker.onmessage = null;
this.worker.onerror = null;
this.worker.terminate();
this.worker = undefined;
}
this.workerScript = undefined;
};
run = ({script, task, responses, onResult, onError}) => {
if(this.disposed){
return undefined;
}
const request = {
id: this.nextRequestID++,
script,
task,
responses,
onResult,
onError,
};
this.latestRequestID = request.id;
this.queuedRequest = request;
if(!this.activeRequest){
this.runQueuedRequest();
}
return request.id;
};
cancel = (requestID) => {
if(this.queuedRequest?.id === requestID){
this.queuedRequest = undefined;
}
if(this.activeRequest?.id === requestID){
this.activeRequest.cancelled = true;
}
if(this.latestRequestID === requestID){
this.latestRequestID = 0;
}
};
runQueuedRequest = () => {
if(this.disposed || this.activeRequest || !this.queuedRequest){
return;
}
const request = this.queuedRequest;
this.queuedRequest = undefined;
this.activeRequest = request;
const message = {
type: "render",
requestID: request.id,
task: request.task,
responses: request.responses,
};
if(request.script !== this.workerScript){
message.script = request.script;
this.workerScript = request.script;
}
request.timeout = setTimeout(() => {
this.finishActiveRequest({
error: new Error(`Browser script exceeded the ${this.timeoutMS}ms time limit`),
restartWorker: true,
});
}, this.timeoutMS);
try {
if(!this.worker){
this.startWorker();
}
this.worker.postMessage(message);
} catch (error) {
this.finishActiveRequest({error, restartWorker: true});
}
};
handleMessage = ({data}) => {
const request = this.activeRequest;
if(!request || data?.requestID !== request.id){
return;
}
if(data.type === "log"){
if(!request.cancelled && request.id === this.latestRequestID && !this.queuedRequest){
console.log(...(Array.isArray(data.args) ? data.args : [data.args]));
}
return;
}
if(data.type === "error"){
if(data.phase === "compile"){
this.workerScript = undefined;
}
this.finishActiveRequest({error: toError(data.error, data.phase)});
return;
}
if(data.type === "result"){
this.finishActiveRequest({result: data.result});
}
};
handleWorkerError = (event) => {
event?.preventDefault?.();
if(!this.activeRequest){
this.restartWorker();
return;
}
this.finishActiveRequest({
error: new Error(event?.message || "Browser script worker failed"),
restartWorker: true,
});
};
finishActiveRequest = ({result, error, restartWorker=false}) => {
const request = this.activeRequest;
if(!request){
return;
}
clearTimeout(request.timeout);
this.activeRequest = undefined;
const isCurrent = !request.cancelled && request.id === this.latestRequestID && !this.queuedRequest;
if(restartWorker){
this.restartWorker();
}
if(isCurrent){
if(error){
request.onError?.(error);
}else{
request.onResult?.(result);
}
}
this.runQueuedRequest();
};
dispose = () => {
this.disposed = true;
if(this.activeRequest){
clearTimeout(this.activeRequest.timeout);
}
this.activeRequest = undefined;
this.queuedRequest = undefined;
this.restartWorker();
};
}
@@ -0,0 +1,144 @@
import {BrowserScriptRunner} from "./BrowserScriptRunner";
class MockWorker {
constructor() {
this.messages = [];
this.terminated = false;
}
postMessage = (message) => {
this.messages.push(message);
};
terminate = () => {
this.terminated = true;
};
respond = (data) => {
this.onmessage?.({data});
};
}
const createRunner = (options = {}) => {
const workers = [];
const workerFactory = jest.fn(() => {
const worker = new MockWorker();
workers.push(worker);
return worker;
});
return {
runner: new BrowserScriptRunner({workerFactory, ...options}),
workerFactory,
workers,
};
};
const makeRequest = (overrides = {}) => ({
script: "function(task, responses) { return {plaintext: responses.join('')}; }",
task: {id: 1},
responses: ["one"],
onResult: jest.fn(),
onError: jest.fn(),
...overrides,
});
describe("BrowserScriptRunner", () => {
test("creates its worker lazily and reuses the compiled script", () => {
const {runner, workerFactory, workers} = createRunner();
expect(workerFactory).not.toHaveBeenCalled();
const first = makeRequest();
const firstID = runner.run(first);
expect(workerFactory).toHaveBeenCalledTimes(1);
expect(workers[0].messages[0]).toMatchObject({
type: "render",
requestID: firstID,
script: first.script,
});
workers[0].respond({type: "result", requestID: firstID, result: {plaintext: "one"}});
const second = makeRequest({responses: ["two"]});
const secondID = runner.run(second);
expect(workerFactory).toHaveBeenCalledTimes(1);
expect(workers[0].messages[1]).toEqual({
type: "render",
requestID: secondID,
task: second.task,
responses: second.responses,
});
workers[0].respond({type: "result", requestID: secondID, result: {plaintext: "two"}});
expect(first.onResult).toHaveBeenCalledWith({plaintext: "one"});
expect(second.onResult).toHaveBeenCalledWith({plaintext: "two"});
runner.dispose();
});
test("coalesces updates while a render is running", () => {
const {runner, workers} = createRunner();
const first = makeRequest({responses: ["one"]});
const second = makeRequest({responses: ["two"]});
const latest = makeRequest({responses: ["three"]});
const firstID = runner.run(first);
runner.run(second);
const latestID = runner.run(latest);
expect(workers[0].messages).toHaveLength(1);
workers[0].respond({type: "result", requestID: firstID, result: {plaintext: "one"}});
expect(first.onResult).not.toHaveBeenCalled();
expect(second.onResult).not.toHaveBeenCalled();
expect(workers[0].messages).toHaveLength(2);
expect(workers[0].messages[1]).toMatchObject({
requestID: latestID,
responses: ["three"],
});
workers[0].respond({type: "result", requestID: latestID, result: {plaintext: "three"}});
expect(latest.onResult).toHaveBeenCalledWith({plaintext: "three"});
runner.dispose();
});
test("restarts a timed-out worker and reports the current failure", () => {
jest.useFakeTimers();
const {runner, workers} = createRunner({timeoutMS: 25});
const request = makeRequest();
runner.run(request);
jest.advanceTimersByTime(25);
expect(workers[0].terminated).toBe(true);
expect(request.onError).toHaveBeenCalledWith(expect.objectContaining({
message: "Browser script exceeded the 25ms time limit",
}));
const next = makeRequest({responses: ["after timeout"]});
runner.run(next);
expect(workers).toHaveLength(2);
expect(workers[1].messages[0].script).toBe(next.script);
runner.dispose();
jest.useRealTimers();
});
test("returns worker errors and reloads scripts after compile failures", () => {
const {runner, workers} = createRunner();
const request = makeRequest();
const requestID = runner.run(request);
workers[0].respond({
type: "error",
requestID,
phase: "compile",
error: {name: "SyntaxError", message: "Unexpected token"},
});
expect(request.onError).toHaveBeenCalledWith(expect.objectContaining({
name: "SyntaxError",
message: "Unexpected token",
phase: "compile",
}));
const retry = makeRequest();
runner.run(retry);
expect(workers[0].messages[1].script).toBe(retry.script);
runner.dispose();
});
});
@@ -0,0 +1,79 @@
// The frame owns only a worker and a private message port. It never evaluates
// service code in its window. The opaque origin denies application storage;
// the blob worker inherits this CSP, which denies all network destinations.
export const sandboxDocument = `<!doctype html>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval'; worker-src blob:; connect-src 'none'; base-uri 'none'; form-action 'none'">
<script>
addEventListener('message', function initialize(event) {
if (event.source !== parent || !event.ports[0]) return;
removeEventListener('message', initialize);
const port = event.ports[0];
try {
const url = URL.createObjectURL(new Blob([event.data], {type: 'text/javascript'}));
const worker = new Worker(url);
URL.revokeObjectURL(url);
worker.onmessage = event => port.postMessage(event.data);
worker.onerror = event => {
event.preventDefault();
port.postMessage({type: 'sandbox-error', message: event.message});
};
port.onmessage = event => worker.postMessage(event.data);
port.start();
} catch (error) {
port.postMessage({type: 'sandbox-error', message: error.message});
}
});
</script>`;
let workerSource;
const loadWorkerSource = () => {
if (!workerSource) {
workerSource = fetch("/new/browser-script-worker.js").then(response => {
if (!response.ok) throw new Error("Failed to load browser script worker");
return response.text();
}).catch(error => {
workerSource = undefined;
throw error;
});
}
return workerSource;
};
// Worker-compatible adapter preserves the runner's queue, cancellation and
// timeout behavior. Removing the frame also terminates its worker.
export const createSandboxedWorker = () => {
const frame = document.createElement("iframe");
frame.setAttribute("sandbox", "allow-scripts");
frame.hidden = true;
frame.title = "Browser script sandbox";
const channel = new MessageChannel();
let disposed = false;
const adapter = {
onmessage: null,
onerror: null,
postMessage: message => channel.port1.postMessage(message),
terminate: () => {
disposed = true;
frame.remove();
channel.port1.close();
channel.port2.close();
},
};
channel.port1.onmessage = event => {
if (disposed) return;
if (event.data?.type === "sandbox-error") {
adapter.onerror?.({message: event.data.message});
} else {
adapter.onmessage?.(event);
}
};
const loaded = new Promise(resolve => { frame.onload = resolve; });
frame.srcdoc = sandboxDocument;
document.body.appendChild(frame);
Promise.all([loaded, loadWorkerSource()]).then(([, source]) => {
if (!disposed) frame.contentWindow.postMessage(source, "*", [channel.port2]);
}).catch(error => {
if (!disposed) adapter.onerror?.(error);
});
return adapter;
};
@@ -0,0 +1,70 @@
import fs from "fs";
import path from "path";
import vm from "vm";
const workerSource = fs.readFileSync(
path.resolve(__dirname, "../../../../public/browser-script-worker.js"),
"utf8",
);
const createWorkerContext = () => {
const messages = [];
const context = {
console: {log: jest.fn()},
self: {
postMessage: (message) => messages.push(message),
},
};
vm.runInNewContext(workerSource, context);
return {messages, worker: context.self};
};
test("browser scripts run without page globals or local storage", () => {
const {messages, worker} = createWorkerContext();
worker.onmessage({
data: {
type: "render",
requestID: 1,
script: `function() {
return {
plaintext: [typeof window, typeof document, typeof localStorage].join(","),
};
}`,
task: {id: 1},
responses: [],
},
});
expect(messages).toEqual([{
type: "result",
requestID: 1,
result: {plaintext: "undefined,undefined,undefined"},
}]);
});
test("the worker reuses its loaded function for later responses", () => {
const {messages, worker} = createWorkerContext();
worker.onmessage({
data: {
type: "render",
requestID: 1,
script: "function(task, responses) { return {plaintext: task.id + responses.join('')}; }",
task: {id: 7},
responses: ["first"],
},
});
worker.onmessage({
data: {
type: "render",
requestID: 2,
task: {id: 8},
responses: ["second"],
},
});
expect(messages[1]).toEqual({
type: "result",
requestID: 2,
result: {plaintext: "8second"},
});
});
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,465 @@
import React from "react";
import {gql, useQuery, useSubscription} from "@apollo/client";
import {getSkewedNow} from "../../utilities/Time";
const browserTreeFragment = gql`
fragment callbackBrowserTreeData on mythictree {
comment
deleted
task_id
filemeta {
id
agent_file_id
filename_text
}
tags {
tagtype {
name
color
id
}
id
}
host
id
os
can_have_children
has_children
success
full_path_text
display_path_text
name_text
timestamp
parent_path_text
tree_type
metadata
callback {
id
display_id
mythictree_groups
}
}
`;
const rootBrowserTreeQuery = gql`
${browserTreeFragment}
query callbackBrowserTreeRoots($tree_type: String!) {
mythictree(
where: {parent_path_text: {_eq: ""}, tree_type: {_eq: $tree_type}}
order_by: {id: asc}
) {
...callbackBrowserTreeData
}
}
`;
const browserTreeSubscription = gql`
${browserTreeFragment}
subscription callbackBrowserTreeStream($now: timestamp!, $tree_type: String!) {
mythictree_stream(
batch_size: 1000
cursor: {initial_value: {timestamp: $now}}
where: {tree_type: {_eq: $tree_type}}
) {
...callbackBrowserTreeData
}
}
`;
const processTreeSubscription = gql`
${browserTreeFragment}
subscription callbackProcessTreeStream($now: timestamp!) {
mythictree_stream(
batch_size: 1000
cursor: {initial_value: {timestamp: $now}}
where: {tree_type: {_eq: "process"}, deleted: {_eq: false}}
) {
...callbackBrowserTreeData
}
}
`;
const getGroups = (row) => row?.callback?.mythictree_groups || ["Unknown Callbacks"];
const decodeFilename = (value) => {
if(!value){return "";}
try{
const text = window.atob(value);
const bytes = Uint8Array.from(text, (character) => character.charCodeAt(0));
return new TextDecoder().decode(bytes);
}catch(error){
try{
return decodeURIComponent(window.atob(value));
}catch(secondError){
return value;
}
}
};
const uniqueBy = (values = [], getKey = (value) => value?.id) => {
const result = [];
const indexes = new Map();
values.forEach((value) => {
const key = getKey(value);
if(key === undefined || key === null){
result.push(value);
return;
}
if(indexes.has(key)){
result[indexes.get(key)] = value;
}else{
indexes.set(key, result.length);
result.push(value);
}
});
return result;
};
const uniquePermissions = (values = []) => uniqueBy(values, (value) => {
if(value && typeof value === "object"){
const keys = Object.keys(value).sort();
return JSON.stringify(value, keys);
}
return `${typeof value}:${value}`;
});
const decodeFilemeta = (values = []) => values.map((file) => ({
...file,
filename_text: decodeFilename(file.filename_text),
}));
const uniqueFilemeta = (values = []) => uniqueBy(values).sort((left, right) => left.id - right.id);
const rowFingerprint = (row) => JSON.stringify({
id: row.id,
task_id: row.task_id,
timestamp: row.timestamp,
deleted: row.deleted,
success: row.success,
comment: row.comment,
has_children: row.has_children,
metadata: row.metadata,
tags: (row.tags || []).map((tag) => tag.id),
filemeta: (row.filemeta || []).map((file) => [file.id, file.filename_text]),
callback: row.callback?.id,
callbackGroups: row.callback?.mythictree_groups,
});
const cloneRow = (row, mode) => ({
...row,
metadata: {...(row.metadata || {})},
tags: uniqueBy(row.tags || []),
filemeta: mode === "file" ? uniqueFilemeta(decodeFilemeta(row.filemeta || [])) : [...(row.filemeta || [])],
...(mode === "process" ? {callbacks: row.callback ? [row.callback] : []} : {}),
});
const mergeFileRow = (existing, row) => {
const next = {...existing};
if((existing.task_id ?? -1) <= (row.task_id ?? -1)){
next.deleted = row.deleted;
}
if((existing.success === null || !existing.success) && row.success !== null){
next.success = row.success;
next.task_id = row.task_id;
}
next.comment = row.comment;
next.tags = uniqueBy([...(existing.tags || []), ...(row.tags || [])]);
next.has_children = row.has_children || existing.has_children;
next.metadata = {...(existing.metadata || {})};
["size", "access_time", "modify_time"].forEach((key) => {
next.metadata[key] = row.metadata?.[key];
});
if(row.metadata?.permissions !== undefined && row.metadata?.permissions !== null){
if(Array.isArray(existing.metadata?.permissions) && Array.isArray(row.metadata.permissions)){
next.metadata.permissions = uniquePermissions([
...existing.metadata.permissions,
...row.metadata.permissions,
]);
}else if(Array.isArray(existing.metadata?.permissions) !== Array.isArray(row.metadata.permissions)){
next.metadata.permissions = row.metadata.permissions;
}else{
next.metadata.permissions = {
...(existing.metadata?.permissions || {}),
...(row.metadata.permissions || {}),
};
}
}
next.filemeta = uniqueFilemeta([...(existing.filemeta || []), ...decodeFilemeta(row.filemeta || [])]);
return next;
};
const mergeCustomRow = (existing, row) => {
if((existing.task_id ?? -1) >= (row.task_id ?? -1)){
return existing;
}
return {
...existing,
deleted: row.deleted,
success: ((existing.success === null || !existing.success) && row.success !== null) ? row.success : existing.success,
comment: row.comment,
tags: uniqueBy(row.tags || []),
has_children: row.has_children || existing.has_children,
metadata: {...(existing.metadata || {}), ...(row.metadata || {})},
display_path_text: row.display_path_text,
task_id: row.task_id,
};
};
const mergeProcessRow = (existing, row) => {
if((existing.task_id ?? -1) <= (row.task_id ?? -1) && row.deleted){
return null;
}
return {
...existing,
has_children: row.has_children || existing.has_children,
callbacks: uniqueBy([...(existing.callbacks || []), ...(row.callback ? [row.callback] : [])]),
comment: `${existing.comment || ""}${row.comment || ""}`,
tags: uniqueBy([...(existing.tags || []), ...(row.tags || [])]),
metadata: (existing.task_id ?? -1) > (row.task_id ?? -1) ?
{...(row.metadata || {}), ...(existing.metadata || {})} :
{...(existing.metadata || {}), ...(row.metadata || {})},
task_id: Math.max(existing.task_id ?? -1, row.task_id ?? -1),
};
};
const mergeRow = (existing, row, mode) => {
if(!existing){
if(mode === "process" && row.deleted){return null;}
return cloneRow(row, mode);
}
if(mode === "file"){
return mergeFileRow(existing, row);
}
if(mode === "process"){
return mergeProcessRow(existing, row);
}
return mergeCustomRow(existing, row);
};
const cloneMatrixBucket = (matrix, group, host, parent, cloned) => {
if(!cloned.root){
matrix = {...matrix};
cloned.root = true;
}
if(!cloned.groups.has(group)){
matrix[group] = {...(matrix[group] || {})};
cloned.groups.add(group);
}
const hostKey = `${group}\u0000${host}`;
if(!cloned.hosts.has(hostKey)){
matrix[group][host] = {...(matrix[group][host] || {})};
cloned.hosts.add(hostKey);
}
const parentKey = `${hostKey}\u0000${parent}`;
if(!cloned.parents.has(parentKey)){
matrix[group][host][parent] = {...(matrix[group][host][parent] || {})};
cloned.parents.add(parentKey);
}
return matrix;
};
export const mergeBrowserTreeRows = (entry, rows, mode = entry.mode) => {
let matrix = entry.matrix;
let changed = false;
const changedBuckets = new Set();
const cloned = {root: false, groups: new Set(), hosts: new Set(), parents: new Set()};
rows.forEach((row) => {
const fingerprint = rowFingerprint(row);
if(entry.fingerprints.get(row.id) === fingerprint){return;}
entry.fingerprints.set(row.id, fingerprint);
getGroups(row).forEach((group) => {
entry.data[group] = entry.data[group] || {};
entry.data[group][row.host] = entry.data[group][row.host] || {};
const existing = entry.data[group][row.host][row.full_path_text];
let next = mergeRow(existing, row, mode);
if(next === existing){return;}
const previousParent = existing?.parent_path_text ?? row.parent_path_text ?? "";
const nextParent = row.parent_path_text ?? previousParent;
if(next && next.parent_path_text !== nextParent){next = {...next, parent_path_text: nextParent};}
matrix = cloneMatrixBucket(matrix, group, row.host, previousParent, cloned);
if(nextParent !== previousParent){
matrix = cloneMatrixBucket(matrix, group, row.host, nextParent, cloned);
delete matrix[group][row.host][previousParent][row.full_path_text];
}
if(next === null){
delete entry.data[group][row.host][row.full_path_text];
delete matrix[group][row.host][previousParent][row.full_path_text];
}else{
entry.data[group][row.host][row.full_path_text] = next;
matrix[group][row.host][nextParent][row.full_path_text] = 1;
}
changedBuckets.add(`${group}\u0000${row.host}\u0000${previousParent}`);
if(nextParent !== previousParent){changedBuckets.add(`${group}\u0000${row.host}\u0000${nextParent}`);}
changed = true;
});
});
if(changed){entry.matrix = matrix;}
return {changed, changedBuckets};
};
const createEntry = (mode) => ({
mode,
data: {},
dataRef: {current: {}},
matrix: {},
version: 0,
hydrated: false,
fingerprints: new Map(),
loadedScopes: new Set(),
listeners: new Set(),
changeListeners: new Set(),
});
export const createCallbackBrowserTreeStore = () => {
const entries = new Map();
const getEntry = (treeType, mode = treeType === "process" ? "process" : treeType === "file" ? "file" : "custom") => {
if(!entries.has(treeType)){
const entry = createEntry(mode);
entry.dataRef.current = entry.data;
entries.set(treeType, entry);
}
return entries.get(treeType);
};
const publish = (entry) => {
entry.version += 1;
entry.listeners.forEach((listener) => listener());
};
return {
getEntry,
mergeRows(treeType, mode, rows, {notify = false, hydrated = false} = {}){
const entry = getEntry(treeType, mode);
const result = mergeBrowserTreeRows(entry, rows, mode);
if(hydrated){entry.hydrated = true;}
if(result.changed || hydrated){publish(entry);}
if(notify && result.changed){
entry.changeListeners.forEach((listener) => listener(result));
}
return {...result, matrix: entry.matrix, data: entry.data};
},
setMatrix(treeType, matrix){
const entry = getEntry(treeType);
entry.matrix = matrix;
publish(entry);
},
subscribe(treeType, listener){
const entry = getEntry(treeType);
entry.listeners.add(listener);
return () => entry.listeners.delete(listener);
},
subscribeToChanges(treeType, listener){
const entry = getEntry(treeType);
entry.changeListeners.add(listener);
return () => entry.changeListeners.delete(listener);
},
getSnapshot(treeType){return getEntry(treeType).version;},
isScopeLoaded(treeType, scope){return getEntry(treeType).loadedScopes.has(scope);},
markScopeLoaded(treeType, scope){getEntry(treeType).loadedScopes.add(scope);},
};
};
const CallbackBrowserTreeContext = React.createContext(null);
const getTreeSources = (openTabs) => {
const sources = new Map();
openTabs.forEach((tab) => {
if(tab.tabType === "fileBrowser"){
sources.set("file", {treeType: "file", mode: "file"});
}else if(tab.tabType === "processBrowser"){
sources.set("process", {treeType: "process", mode: "process"});
}else if(tab.tabType === "customFileBasedBrowser" && tab.customBrowser?.name){
sources.set(tab.customBrowser.name, {treeType: tab.customBrowser.name, mode: "custom"});
}
});
return [...sources.values()];
};
const CallbackBrowserTreeSource = ({treeType, mode, store}) => {
const fromNow = React.useRef(getSkewedNow());
useQuery(rootBrowserTreeQuery, {
variables: {tree_type: treeType},
fetchPolicy: "no-cache",
onCompleted: (data) => store.mergeRows(treeType, mode, data.mythictree || [], {hydrated: true}),
});
useSubscription(mode === "process" ? processTreeSubscription : browserTreeSubscription, {
variables: mode === "process" ? {now: fromNow.current} : {now: fromNow.current, tree_type: treeType},
fetchPolicy: "no-cache",
ignoreResults: true,
onData: ({data}) => store.mergeRows(treeType, mode, data.data?.mythictree_stream || [], {notify: true}),
});
return null;
};
export const CallbackBrowserTreeProvider = ({openTabs, children}) => {
const storeRef = React.useRef();
if(!storeRef.current){storeRef.current = createCallbackBrowserTreeStore();}
const sources = React.useMemo(() => getTreeSources(openTabs), [openTabs]);
return (
<CallbackBrowserTreeContext.Provider value={storeRef.current}>
{sources.map((source) => (
<CallbackBrowserTreeSource key={source.treeType} {...source} store={storeRef.current} />
))}
{children}
</CallbackBrowserTreeContext.Provider>
);
};
const emptySubscribe = () => () => {};
export const useCallbackBrowserTree = ({treeType, mode, active, onInactiveChange}) => {
const store = React.useContext(CallbackBrowserTreeContext);
if(!store){throw new Error("useCallbackBrowserTree must be used inside CallbackBrowserTreeProvider");}
store.getEntry(treeType, mode);
const subscribe = React.useCallback(
(listener) => active ? store.subscribe(treeType, listener) : emptySubscribe(listener),
[active, store, treeType]
);
React.useSyncExternalStore(
subscribe,
React.useCallback(() => store.getSnapshot(treeType), [store, treeType]),
React.useCallback(() => store.getSnapshot(treeType), [store, treeType]),
);
React.useEffect(() => store.subscribeToChanges(treeType, () => {
if(!active){onInactiveChange?.();}
}), [active, onInactiveChange, store, treeType]);
const entry = store.getEntry(treeType, mode);
return {
treeRootDataRef: entry.dataRef,
treeAdjMtx: entry.matrix,
version: entry.version,
hydrated: entry.hydrated,
mergeRows: React.useCallback(
(rows, options) => store.mergeRows(treeType, mode, rows, options),
[mode, store, treeType]
),
setTreeAdjMtx: React.useCallback(
(matrix) => store.setMatrix(treeType, matrix),
[store, treeType]
),
isScopeLoaded: React.useCallback(
(scope) => store.isScopeLoaded(treeType, scope),
[store, treeType]
),
markScopeLoaded: React.useCallback(
(scope) => store.markScopeLoaded(treeType, scope),
[store, treeType]
),
};
};
export const getDefaultBrowserSelection = (treeData, matrix, callbackID, preferredHost) => {
const groups = Object.keys(matrix).sort();
if(groups.length === 0){return {group: "Default", host: preferredHost || ""};}
let callbackGroup;
for(const group of groups){
for(const hostNodes of Object.values(treeData[group] || {})){
for(const node of Object.values(hostNodes || {})){
if(node?.callback?.id === callbackID){callbackGroup = group; break;}
}
if(callbackGroup){break;}
}
if(callbackGroup){break;}
}
const group = callbackGroup || (groups.includes("Default") ? "Default" : groups[0]);
const hosts = Object.keys(matrix[group] || {}).sort();
return {group, host: hosts.includes(preferredHost) ? preferredHost : (hosts[0] || preferredHost || "")};
};
@@ -0,0 +1,141 @@
jest.mock("../../utilities/Time", () => ({getSkewedNow: () => "2026-01-01T00:00:00Z"}));
import {createCallbackBrowserTreeStore} from "./CallbackBrowserTreeStore";
const callback = {id: 7, display_id: 7, mythictree_groups: ["Default"]};
const row = (overrides = {}) => ({
id: 1,
task_id: 1,
timestamp: "2026-01-01T00:00:00Z",
deleted: false,
success: true,
comment: "first",
tags: [{id: 1, tagtype: {id: 1, name: "a", color: "red"}}],
filemeta: [],
host: "HOST",
can_have_children: true,
has_children: true,
full_path_text: "/",
name_text: "/",
parent_path_text: "",
tree_type: "file",
metadata: {size: 1, permissions: ["r"]},
callback,
...overrides,
});
describe("callback browser tree store", () => {
test("merges a streamed file update with the existing precedence", () => {
const store = createCallbackBrowserTreeStore();
store.mergeRows("file", "file", [row()], {hydrated: true});
store.mergeRows("file", "file", [row({
id: 2,
task_id: 2,
timestamp: "2026-01-01T00:00:01Z",
deleted: true,
success: false,
comment: "new",
tags: [{id: 2, tagtype: {id: 2, name: "b", color: "blue"}}],
metadata: {size: 2, access_time: 3, modify_time: 4, permissions: ["w"]},
})], {notify: true});
const node = store.getEntry("file").data.Default.HOST["/"];
expect(node.deleted).toBe(true);
expect(node.comment).toBe("new");
expect(node.tags.map((tag) => tag.id)).toEqual([1, 2]);
expect(node.metadata).toMatchObject({size: 2, access_time: 3, modify_time: 4, permissions: ["r", "w"]});
});
test("ignores exact replay and emits no duplicate notification", () => {
const store = createCallbackBrowserTreeStore();
const listener = jest.fn();
store.subscribeToChanges("file", listener);
const streamedRow = row();
store.mergeRows("file", "file", [streamedRow], {notify: true});
store.mergeRows("file", "file", [streamedRow], {notify: true});
expect(listener).toHaveBeenCalledTimes(1);
expect(store.getEntry("file").data.Default.HOST["/"].tags).toHaveLength(1);
});
test("deduplicates tags, callbacks, and file metadata while preserving newer values", () => {
const store = createCallbackBrowserTreeStore();
store.mergeRows("process", "process", [row({tree_type: "process", comment: "a"})]);
store.mergeRows("process", "process", [row({
id: 2,
timestamp: "2026-01-01T00:00:01Z",
tree_type: "process",
comment: "b",
})]);
const process = store.getEntry("process").data.Default.HOST["/"];
expect(process.callbacks).toHaveLength(1);
expect(process.tags).toHaveLength(1);
expect(process.comment).toBe("ab");
store.mergeRows("file", "file", [row({filemeta: [{id: 3, filename_text: ""}]})]);
store.mergeRows("file", "file", [row({
id: 3,
timestamp: "2026-01-01T00:00:02Z",
filemeta: [{id: 3, filename_text: ""}],
})]);
expect(store.getEntry("file").data.Default.HOST["/"].filemeta).toHaveLength(1);
});
test("does not decode an already-normalized filename again on later updates", () => {
const store = createCallbackBrowserTreeStore();
store.mergeRows("file", "file", [row({filemeta: [{id: 3, filename_text: "dGVzdA=="}]})]);
store.mergeRows("file", "file", [row({
id: 2,
task_id: 2,
timestamp: "2026-01-01T00:00:01Z",
filemeta: [],
})]);
expect(store.getEntry("file").data.Default.HOST["/"].filemeta[0].filename_text).toBe("test");
});
test("updates only the affected adjacency branches", () => {
const store = createCallbackBrowserTreeStore();
store.mergeRows("file", "file", [
row(),
row({id: 2, host: "OTHER", full_path_text: "C:\\", timestamp: "2026-01-01T00:00:01Z"}),
]);
const before = store.getEntry("file").matrix;
const untouchedHost = before.Default.OTHER;
store.mergeRows("file", "file", [row({
id: 3,
full_path_text: "/tmp",
parent_path_text: "/",
timestamp: "2026-01-01T00:00:02Z",
})]);
const after = store.getEntry("file").matrix;
expect(after).not.toBe(before);
expect(after.Default.OTHER).toBe(untouchedHost);
expect(after.Default.HOST["/"]["/tmp"]).toBe(1);
});
test("moves an existing node between parent buckets without rebuilding unrelated hosts", () => {
const store = createCallbackBrowserTreeStore();
store.mergeRows("custom", "custom", [row({
tree_type: "custom",
full_path_text: "/child",
parent_path_text: "/old",
})]);
store.mergeRows("custom", "custom", [row({
tree_type: "custom",
task_id: 2,
timestamp: "2026-01-01T00:00:01Z",
full_path_text: "/child",
parent_path_text: "/new",
})]);
const matrix = store.getEntry("custom").matrix.Default.HOST;
expect(matrix["/old"]["/child"]).toBeUndefined();
expect(matrix["/new"]["/child"]).toBe(1);
});
test("historical hydration does not emit a background notification", () => {
const store = createCallbackBrowserTreeStore();
const listener = jest.fn();
store.subscribeToChanges("custom", listener);
store.mergeRows("custom", "custom", [row({tree_type: "custom"})], {hydrated: true});
expect(listener).not.toHaveBeenCalled();
expect(store.getEntry("custom").hydrated).toBe(true);
});
});
@@ -16,6 +16,14 @@ mutation hideCallback ($callback_display_id: Int!){
}
}
`;
export const updateCallbackTriggerMutation = gql`
mutation updateCallbackTriggerMutation ($callback_display_id: Int!, $trigger_on_checkin_after_time: Int!){
updateCallback(input: {callback_display_id: $callback_display_id, trigger_on_checkin_after_time: $trigger_on_checkin_after_time}) {
status
error
}
}
`;
export const toggleHideCallbackMutations = gql`
mutation hideCallback ($callback_display_id: Int!, $active: Boolean!){
updateCallback(input: {callback_display_id: $callback_display_id, active: $active}) {
@@ -27,7 +35,7 @@ mutation hideCallback ($callback_display_id: Int!, $active: Boolean!){
export const removeEdgeMutation = gql`
mutation removeEdgeMutation ($edge_id: Int!){
callbackgraphedge_remove(edge_id: $edge_id) {
callbackgraphedgeRemove(edge_id: $edge_id) {
status
error
}
@@ -36,7 +44,7 @@ mutation removeEdgeMutation ($edge_id: Int!){
export const addEdgeMutation = gql`
mutation addEdgeMutation ($source_id: Int!, $destination_id: Int!, $c2profile: String!){
callbackgraphedge_add(c2profile: $c2profile, destination_id: $destination_id, source_id: $source_id) {
callbackgraphedgeAdd(c2profile: $c2profile, destination_id: $destination_id, source_id: $source_id) {
status
error
}
@@ -59,8 +67,8 @@ mutation lockCallack($callback_display_id: Int!){
}
`;
export const updateDescriptionCallbackMutation = gql`
mutation updateDescriptionCallack($callback_display_id: Int!, $description: String!){
updateCallback(input: {callback_display_id: $callback_display_id, description: $description}) {
mutation updateDescriptionCallback($callback_display_id: Int!, $description: String!, $color: String!){
updateCallback(input: {callback_display_id: $callback_display_id, description: $description, color: $color}) {
status
error
}
@@ -87,6 +95,9 @@ export const taskingDataFragment = gql`
fragment taskData on task {
comment
parent_task_id
agent_task_id
tasking_location
apitokens_id
callback {
display_id
id
@@ -96,11 +107,23 @@ export const taskingDataFragment = gql`
domain
ip
mythictree_groups
pid
}
callback_id
commentOperator{
username
}
eventstepinstance {
eventgroupinstance {
eventgroup{
id
}
id
}
eventstep {
name
}
}
completed
id
display_id
@@ -111,20 +134,38 @@ export const taskingDataFragment = gql`
display_params
status
timestamp
status_timestamp_submitted
status_timestamp_processing
status_timestamp_preprocessing
command {
cmd
supported_ui_features
id
payloadtype {
name
}
}
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
tasks(where: {is_interactive_task: {_eq: false}}, order_by: {id: asc}) {
id
comment
agent_task_id
apitokens_id
commentOperator{
username
}
@@ -137,6 +178,7 @@ export const taskingDataFragment = gql`
domain
ip
mythictree_groups
pid
}
completed
subtask_group_name
@@ -152,6 +194,9 @@ export const taskingDataFragment = gql`
cmd
supported_ui_features
id
payloadtype {
name
}
}
command_name
response_count
@@ -166,6 +211,7 @@ export const taskingDataFragment = gql`
tasks(order_by: {id: asc}) {
id
}
has_intercepted_response
}
response_count
tags {
@@ -182,11 +228,20 @@ export const taskingDataFragment = gql`
}
`;
export const createTaskingMutation = gql`
mutation createTasking($callback_id: Int, $callback_ids: [Int], $command: String!, $params: String!, $files: [String], $token_id: Int, $tasking_location: String, $original_params: String, $parameter_group_name: String, $parent_task_id: Int, $is_interactive_task: Boolean, $interactive_task_type: Int) {
createTask(callback_id: $callback_id, callback_ids: $callback_ids, command: $command, params: $params, files: $files, token_id: $token_id, tasking_location: $tasking_location, original_params: $original_params, parameter_group_name: $parameter_group_name, parent_task_id: $parent_task_id, is_interactive_task: $is_interactive_task, interactive_task_type: $interactive_task_type) {
mutation createTasking($callback_display_id: Int, $callback_display_ids: [Int], $command: String!, $params: String!, $files: [String], $token_id: Int, $tasking_location: String, $original_params: String, $parameter_group_name: String, $parent_task_display_id: Int, $is_interactive_task: Boolean, $interactive_task_type: Int, $payload_type: String, $resolve_task_references: Boolean) {
createTask(callback_display_id: $callback_display_id, callback_display_ids: $callback_display_ids, command: $command, params: $params, files: $files, token_id: $token_id, tasking_location: $tasking_location, original_params: $original_params, parameter_group_name: $parameter_group_name, parent_task_display_id: $parent_task_display_id, is_interactive_task: $is_interactive_task, interactive_task_type: $interactive_task_type, payload_type: $payload_type, resolve_task_references: $resolve_task_references) {
status
id
error
}
}
`;
export const exportCallbackConfigQuery = gql`
query exportCallbackConfigQuery($callback_display_id: Int!) {
exportCallbackConfig(callback_display_id: $callback_display_id) {
status
error
config
}
}
`;
@@ -0,0 +1,110 @@
const EMPTY_CHILDREN = Object.freeze([]);
const comparableTaskFields = [
"comment", "completed", "display_params", "original_params", "status", "timestamp",
"response_count", "opsec_pre_blocked", "opsec_pre_bypassed", "opsec_post_blocked",
"opsec_post_bypassed", "has_intercepted_response", "tasking_location",
];
const relationIDsEqual = (left = [], right = []) => {
if(left.length !== right.length){
return false;
}
for(let index = 0; index < left.length; index++){
if(left[index]?.id !== right[index]?.id){
return false;
}
}
return true;
};
export const taskRowsEqual = (left, right) => {
if(left === right){
return true;
}
if(!left || !right || left.id !== right.id){
return false;
}
for(const field of comparableTaskFields){
if(left[field] !== right[field]){
return false;
}
}
return left.command?.id === right.command?.id &&
left.operator?.username === right.operator?.username &&
left.commentOperator?.username === right.commentOperator?.username &&
relationIDsEqual(left.tags, right.tags) &&
relationIDsEqual(left.tasks, right.tasks);
};
export const mergeTasksByID = (existing = [], incoming = []) => {
if(incoming.length === 0){
return existing;
}
const tasksByID = new Map(existing.map((task) => [task.id, task]));
incoming.forEach((task) => {
const current = tasksByID.get(task.id);
tasksByID.set(task.id, taskRowsEqual(current, task) ? current : task);
});
const merged = Array.from(tasksByID.values()).sort((left, right) => left.id - right.id);
if(merged.length === existing.length && merged.every((task, index) => task === existing[index])){
return existing;
}
return merged;
};
export const nextLiveTaskLimit = (currentLimit, liveResultCount, increment = 20) => {
return liveResultCount >= currentLimit ? currentLimit + increment : currentLimit;
};
export const createTaskChildrenStore = () => {
const childrenByParent = new Map();
const listenersByParent = new Map();
let highestTaskID = 0;
const notify = (parentID) => {
listenersByParent.get(parentID)?.forEach((listener) => listener());
};
const merge = (tasks = []) => {
const incomingByParent = new Map();
tasks.forEach((task) => {
if(task?.parent_task_id === null || task?.parent_task_id === undefined){
return;
}
highestTaskID = Math.max(highestTaskID, task.id || 0);
const parentTasks = incomingByParent.get(task.parent_task_id) || [];
parentTasks.push(task);
incomingByParent.set(task.parent_task_id, parentTasks);
});
const changedParents = [];
incomingByParent.forEach((incoming, parentID) => {
const current = childrenByParent.get(parentID) || EMPTY_CHILDREN;
const merged = mergeTasksByID(current, incoming);
if(merged !== current){
childrenByParent.set(parentID, merged);
changedParents.push(parentID);
}
});
changedParents.forEach(notify);
return changedParents;
};
return {
getHighestTaskID: () => highestTaskID,
getSnapshot: (parentID) => childrenByParent.get(parentID) || EMPTY_CHILDREN,
merge,
subscribe: (parentID, listener) => {
const listeners = listenersByParent.get(parentID) || new Set();
listeners.add(listener);
listenersByParent.set(parentID, listeners);
return () => {
listeners.delete(listener);
if(listeners.size === 0){
listenersByParent.delete(parentID);
}
};
},
};
};
@@ -0,0 +1,45 @@
import {createTaskChildrenStore, mergeTasksByID, nextLiveTaskLimit} from "./CallbackTaskingStreamUtils";
describe("callback task stream utilities", () => {
test("merges tasks by id while preserving unchanged row identity", () => {
const first = {id: 1, status: "complete", tasks: [], tags: [], command: {id: 1}, operator: {username: "a"}};
const second = {id: 2, status: "running", tasks: [], tags: [], command: {id: 2}, operator: {username: "a"}};
const merged = mergeTasksByID([first, second], [
{...first},
{...second, status: "complete"},
{id: 3, status: "submitted", tasks: [], tags: [], command: {id: 3}, operator: {username: "a"}},
]);
expect(merged.map((task) => task.id)).toEqual([1, 2, 3]);
expect(merged[0]).toBe(first);
expect(merged[1]).not.toBe(second);
});
test("grows the live limit only when the live result window is saturated", () => {
expect(nextLiveTaskLimit(20, 19)).toBe(20);
expect(nextLiveTaskLimit(20, 20)).toBe(40);
expect(nextLiveTaskLimit(40, 40)).toBe(60);
});
test("groups children, deduplicates replay, and notifies only changed parents", () => {
const store = createTaskChildrenStore();
const parentOneListener = jest.fn();
const parentTwoListener = jest.fn();
store.subscribe(10, parentOneListener);
store.subscribe(20, parentTwoListener);
const child = {id: 2, parent_task_id: 10, status: "running", tasks: [], tags: []};
store.merge([child, {id: 3, parent_task_id: 20, status: "complete", tasks: [], tags: []}]);
expect(store.getSnapshot(10)).toEqual([child]);
expect(parentOneListener).toHaveBeenCalledTimes(1);
expect(parentTwoListener).toHaveBeenCalledTimes(1);
store.merge([{...child}]);
expect(parentOneListener).toHaveBeenCalledTimes(1);
store.merge([{...child, status: "complete"}]);
expect(parentOneListener).toHaveBeenCalledTimes(2);
expect(parentTwoListener).toHaveBeenCalledTimes(1);
expect(store.getHighestTaskID()).toBe(3);
});
});
@@ -1,60 +1,60 @@
import {MythicActionButton} from "../../MythicComponents/MythicActionButton";
import React, { useEffect } from 'react';
import { styled } from '@mui/material/styles';
import { CallbacksTabs } from './CallbacksTabs';
import SpeedDial from '@mui/material/SpeedDial';
import SpeedDialIcon from '@mui/material/SpeedDialIcon';
import SpeedDialAction from '@mui/material/SpeedDialAction';
import TocIcon from '@mui/icons-material/Toc';
import AssessmentIcon from '@mui/icons-material/Assessment';
import { CallbacksTop } from './CallbacksTop';
import Split from 'react-split';
import PhoneForwardedIcon from '@mui/icons-material/PhoneForwarded';
import {MythicDialog} from "../../MythicComponents/MythicDialog";
import {ImportCallbackConfigDialog} from "./ImportCallbackConfigDialog";
import {reorder} from "../../MythicComponents/MythicDraggableList";
import Paper from '@mui/material/Paper';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {useTheme} from '@mui/material/styles';
const PREFIX = 'Callbacks';
const classes = {
root: `${PREFIX}-root`,
speedDial: `${PREFIX}-speedDial`,
speedDialAction: `${PREFIX}-speedDialAction`,
tooltip: `${PREFIX}-tooltip`,
arrow: `${PREFIX}-arrow`
};
const StyledSpeedDial = styled(SpeedDial)(({theme}) => ({
[`&.${classes.speedDial}`]: {
position: 'absolute',
'&.MuiSpeedDial-directionUp, &.MuiSpeedDial-directionLeft': {
bottom: theme.spacing(2),
right: theme.spacing(2),
},
'&.MuiSpeedDial-directionDown, &.MuiSpeedDial-directionRight': {
top: theme.spacing(2),
right: theme.spacing(2),
},
zIndex: 1,
},
[`& .${classes.speedDialAction}`]: {
backgroundColor: theme.palette.speedDialAction,
},
[`& .${classes.tooltip}`]: {
backgroundColor: theme.palette.background.contrast,
color: theme.palette.text.contrast,
boxShadow: theme.shadows[1],
fontSize: 13,
},
[`& .${classes.arrow}`]: {
color: theme.palette.background.contrast,
export const getCallbackIdFromClickedTab = (tabId) => {
if(tabId === null || tabId === undefined){return 0}
if(tabId === ""){return 0}
if(tabId.includes("fileBrowser")) {
return Number(tabId.split("fileBrowser")[0]);
}else if(tabId.includes("interact")){
return Number(tabId.split("interact")[0]);
}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;
}
}));
}
export function Callbacks({me}) {
const theme = useTheme();
const [openCallbackImport, setOpenCallbackImport] = React.useState(false);
const [topDisplay, setTopDisplay] = React.useState('table');
const [newDataForTab, setNewDataForTab] = React.useState({});
const [openTabs, setOpenTabs] = React.useState([]);
const [clickedTabId, setClickedTabId] = React.useState('');
const [clickedTabId, setClickedTabIdValue] = React.useState('');
const openTabRef = React.useRef([]);
const callbackTableGridRef = React.useRef();
const [callbackTableSplitSizes, setCallbackTableSplitSizes] = React.useState([30, 70]);
const setClickedTabId = (tabID) => {
if(callbackTableGridRef.current){
let tabIDNumber = getCallbackIdFromClickedTab(tabID);
let rowIndex = callbackTableGridRef.current?.props?.itemData?.items?.findIndex((e) => {
return e[0]?.props?.rowData?.id === tabIDNumber
});
if(rowIndex >= 0){
callbackTableGridRef.current?.scrollToItem({rowIndex: rowIndex, align: "end", columnIndex: 0})
}
}
setClickedTabIdValue(tabID);
setNewDataForTab((prev) => {return {...prev, [tabID]: false}});
}
useEffect(() => {
const oldTabs = localStorage.getItem('openTabs');
if (oldTabs !== undefined && oldTabs !== null) {
@@ -69,12 +69,26 @@ export function Callbacks({me}) {
console.log('failed to parse oldTabs', error);
}
}
const oldSizes = localStorage.getItem("callbackTableSplitSizes");
if (oldSizes) {
try{
setCallbackTableSplitSizes(JSON.parse(oldSizes));
}catch(error){
console.log("failed to parse callback table split sizes");
}
}
}, []);
useEffect( () => {
openTabRef.current = openTabs;
}, [openTabs])
const onOpenTab = React.useRef( (tabData) => {
let found = false;
openTabRef.current = openTabRef.current.map( (tab) => {
if(tab.tabID === tabData.tabID){
return {...tabData};
}
return {...tab};
})
openTabRef.current.forEach((tab) => {
if (tab.tabID === tabData.tabID) found = true;
});
@@ -83,10 +97,29 @@ export function Callbacks({me}) {
const tabs = [...openTabRef.current, { ...tabData }];
localStorage.setItem('openTabs', JSON.stringify(tabs));
setOpenTabs(tabs);
} else {
setOpenTabs([...openTabRef.current]);
}
localStorage.setItem('clickedTab', tabData.tabID);
setClickedTabId(tabData.tabID);
});
const onOpenTabs = React.useRef( (tabData) => {
let currentTabs = [...openTabRef.current];
for(let i = 0; i < tabData.length; i++){
let found = false;
currentTabs.forEach((tab) => {
if (tab.tabID === tabData[i].tabID) found = true;
});
if (!found) {
currentTabs = [...currentTabs, { ...tabData[i] }];
}
}
localStorage.setItem('openTabs', JSON.stringify(currentTabs));
setOpenTabs(currentTabs);
localStorage.setItem('clickedTab', tabData[0].tabID);
setClickedTabId(tabData[0].tabID);
});
const onEditTabDescription = React.useCallback( (tabInfo, description) => {
const tabs = openTabs.map((t) => {
@@ -105,6 +138,10 @@ export function Callbacks({me}) {
});
localStorage.setItem('openTabs', JSON.stringify(tabSet));
setOpenTabs(tabSet);
if(tabSet.length === 0){
setClickedTabId("0");
localStorage.removeItem("clickedTab");
}
}, [openTabs]);
const onDragTab = ({selected, toLeftOf}) => {
//console.log("onDragTab in CallbacksTabs", selected, toLeftOf);
@@ -133,7 +170,7 @@ export function Callbacks({me}) {
newOpenTabList.push(openTabs[targetTabIndex]);
newOpenTabList.push(openTabs[i]);
}
setClickedTabId(openTabs[targetTabIndex].tabID)
} else {
newOpenTabList.push(openTabs[i]);
@@ -166,108 +203,99 @@ export function Callbacks({me}) {
}
},
];
const onDragEnd = ({ destination, source }) => {
// dropped outside the list
if (!destination) return;
const newItems = reorder(openTabs, source.index, destination.index);
setOpenTabs(newItems);
localStorage.setItem('openTabs', JSON.stringify(newItems));
};
return (
<>
<SpeedDialWrapper setTopDisplay={setTopDisplay} />
<Split direction="vertical" sizes={[30, 70]} minSize={[0,0]} style={{ height: "100%" }}>
<div className="bg-gray-base">
<CallbacksTop topDisplay={topDisplay} onOpenTab={onOpenTab.current} me={me}/>
<Split direction="vertical"
sizes={callbackTableSplitSizes}
minSize={[0,0]}
onDragEnd={(sizes) => localStorage.setItem('callbackTableSplitSizes', JSON.stringify(sizes))}
style={{ height: "100%" }}>
<div style={{display: "flex", flexDirection: "row-reverse", height: "100%", minWidth: 0, overflow: "hidden"}}>
<Paper elevation={0} style={{
width: "2.25rem",
display: "flex",
gap: "0.25rem",
flexDirection: "column",
alignItems: "center",
overflow: "hidden",
backgroundColor: theme.palette.background.paper,
borderTop: `1px solid ${theme.borderColor}`,
borderRight: 0,
borderBottom: `1px solid ${theme.borderColor}`,
borderLeft: `1px solid ${theme.borderColor}`,
borderRadius: `${theme.shape.borderRadius}px 0 0 ${theme.shape.borderRadius}px`,
paddingTop: "3px",
}}>
{topDisplay !== 'table' &&
<MythicActionButton
iconOnly
appearance={"raised"}
shape={"square"}
variant={"outlined"}
icon={<TocIcon />}
tooltip={"Table View"}
onClick={() =>setTopDisplay("table")}>
</MythicActionButton>
}
{topDisplay !== 'graph' &&
<MythicActionButton
iconOnly
appearance={"raised"}
shape={"square"}
variant={"outlined"}
icon={<AssessmentIcon />}
tooltip={"Graph View"}
onClick={() =>setTopDisplay("graph")}>
</MythicActionButton>
}
<MythicActionButton
appearance={"raised"}
iconOnly
shape={"square"}
colorMode={"hover"}
tone={"neutral"}
icon={<PhoneForwardedIcon />}
tooltip={"Import previously exported Callbacks"}
onClick={() =>setOpenCallbackImport(true)}>
</MythicActionButton>
{openCallbackImport &&
<MythicDialog fullWidth={true} maxWidth="sm" open={openCallbackImport}
onClose={()=>{setOpenCallbackImport(false);}}
innerDialog={<ImportCallbackConfigDialog onClose={()=>{setOpenCallbackImport(false);}} />}
/>
}
</Paper>
<CallbacksTop
callbackTableGridRef={callbackTableGridRef}
topDisplay={topDisplay}
onOpenTab={onOpenTab.current}
onOpenTabs={onOpenTabs.current}
me={me} clickedTabId={clickedTabId}/>
</div>
<div className="bg-gray-mid">
<div >
<CallbacksTabs
onCloseTab={onCloseTab}
onEditTabDescription={onEditTabDescription}
key={'callbackstabs'}
clickedTabId={clickedTabId}
setClickedTabId={setClickedTabId}
openTabs={openTabs}
onDragTab={onDragTab}
me={me}
onDragEnd={onDragEnd}
contextMenuOptions={contextMenuOptions}
newDataForTab={newDataForTab}
setNewDataForTab={setNewDataForTab}
/>
</div>
</Split>
</>
);
}
/*
<div style={{ maxWidth: '100%', height: '100%', flexDirection: 'column'}}>
<React.Fragment>
<SpeedDialWrapper setTopDisplay={setTopDisplay} heights={heights} onSubmitHeights={onSubmitHeights} />
<div style={{flexGrow: 1, flexBasis: heights.top, height: heights.top }}>
<CallbacksTop topDisplay={topDisplay} onOpenTab={onOpenTab.current} heights={heights} me={me}/>
</div>
<div style={{ flexGrow: 1, flexBasis: heights.bottom, height: heights.bottom }}>
<CallbacksTabs
onCloseTab={onCloseTab}
onEditTabDescription={onEditTabDescription}
tabHeight={heights.bottom}
maxHeight={heights.bottom}
key={'callbackstabs'}
clickedTabId={clickedTabId}
openTabs={openTabs}
onDragTab={onDragTab}
me={me}
contextMenuOptions={contextMenuOptions}
/>
</div>
</React.Fragment>
</div>
*/
function SpeedDialWrapperPreMemo({ setTopDisplay }) {
const [open, setOpen] = React.useState(false);
const actions = React.useMemo(
() => [
{
icon: <TocIcon />,
name: 'Table layout',
onClick: () => {
setTopDisplay('table');
},
},
{
icon: <AssessmentIcon />,
name: 'Graph layout',
onClick: () => {
setTopDisplay('graph');
},
},
],
[] // eslint-disable-line react-hooks/exhaustive-deps
);
return (
<React.Fragment>
<StyledSpeedDial
ariaLabel='SpeedDial example'
className={classes.speedDial}
icon={<SpeedDialIcon />}
onClose={() => {
setOpen(false);
}}
onOpen={() => {
setOpen(true);
}}
FabProps={{ color: 'info', size: "small", variant: "extended" }}
open={open}
style={{ marginTop:"35px" }}
direction='down'>
{actions.map((action) => (
<SpeedDialAction
arrow
className={classes.speedDialAction}
key={action.name}
TooltipClasses={{ ".MuiTooltip-tooltip": classes.tooltip,
".MuiTooltip-tooltipArrow": classes.arrow,
}}
icon={action.icon}
tooltipTitle={action.name}
onClick={action.onClick}
/>
))}
</StyledSpeedDial>
</React.Fragment>
);
}
const SpeedDialWrapper = React.memo(SpeedDialWrapperPreMemo);
@@ -1,16 +1,9 @@
import React, {useRef, useEffect, useState, useMemo, useContext} from 'react';
import React, {useEffect, useState, useMemo, useContext} from 'react';
import {DrawC2PathElementsFlowWithProvider} from './C2PathDialog';
import {Button} from '@mui/material';
import ButtonGroup from '@mui/material/ButtonGroup';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import Paper from '@mui/material/Paper';
import Grow from '@mui/material/Grow';
import Popper from '@mui/material/Popper';
import {MythicActionButton} from '../../MythicComponents/MythicActionButton';
import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import {useMutation } from '@apollo/client';
import {saveSvgAsPng} from 'save-svg-as-png';
import {hideCallbackMutation, removeEdgeMutation, addEdgeMutation} from './CallbackMutations';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
import {MythicSelectFromListDialog} from '../../MythicComponents/MythicSelectFromListDialog';
@@ -24,7 +17,10 @@ import OutlinedInput from '@mui/material/OutlinedInput';
import InputLabel from '@mui/material/InputLabel';
import FormControl from '@mui/material/FormControl';
import Select from '@mui/material/Select';
import {CallbackGraphEdgesContext, OnOpenTabContext} from './CallbacksTop';
import {CallbackGraphEdgesContext, CallbacksContext} from './CallbacksTop';
import {GetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
import {operatorSettingDefaults} from "../../../cache";
import {useTaskReferenceSubmitter} from './taskingReferences';
const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 1;
@@ -62,12 +58,10 @@ query loadedLinkCommandsQuery ($callback_id: Int!){
const GraphViewOptions = ({viewConfig, setViewConfig}) => {
const theme = useTheme();
const dropdownAnchorRef = React.useRef(null);
const [dropdownOpen, setDropdownOpen] = React.useState(false);
const [showConfiguration, setShowConfiguration] = React.useState(false);
const labelComponentOptions = ["display_id", "user", "host", "ip", "domain", "os", "process_name"];
const [selectedComponentOptions, setSelectedComponentOptions] = React.useState(["display_id", "user"]);
const [selectedGroupBy, setSelectedGroupBy] = React.useState("None");
const [selectedComponentOptions, setSelectedComponentOptions] = React.useState(viewConfig?.label_components || ["display_id", "user"]);
const [selectedGroupBy, setSelectedGroupBy] = React.useState(viewConfig?.group_by || "None");
const groupByOptions = ["host", "user", "ip", "domain", "os", "process_name", "None"];
const handleChange = (event) => {
const {
@@ -87,135 +81,117 @@ const GraphViewOptions = ({viewConfig, setViewConfig}) => {
useEffect( () => {
setViewConfig({...viewConfig, group_by: selectedGroupBy});
}, [selectedGroupBy])
const handleDropdownToggle = (evt) => {
evt.stopPropagation();
setDropdownOpen((prevOpen) => !prevOpen);
};
const handleMenuItemClick = (event, index) => {
options[index].click();
setDropdownOpen(false);
};
const options = [
{name: showConfiguration ? "Hide Grouping Options": "Show Grouping Options", click: () => {
setShowConfiguration(!showConfiguration);
}},
{name: viewConfig["include_disconnected"] ? 'Show Only Active Edges' : "Show All Edges", click: () => {
const view = {...viewConfig, include_disconnected: !viewConfig["include_disconnected"]};
setViewConfig(view);
}},
{name: viewConfig["show_all_nodes"] ? 'Hide inactive callbacks' : 'Show All Callbacks', click: () => {
const view = {...viewConfig, show_all_nodes: !viewConfig["show_all_nodes"]};
setViewConfig(view);
}},
{name: viewConfig["rankDir"] === "LR" ? 'Change Layout to Top-Bottom' : "Change Layout to Left-Right", click: () => {
if(viewConfig["rankDir"] === "LR"){
const view = {...viewConfig, rankDir: "TB"};
setViewConfig(view);
}else{
const view = {...viewConfig, rankDir: "LR"};
setViewConfig(view);
}
}},
{name: viewConfig["packet_flow_view"] ? "View Connection Directions" : "View Egress Routes" , click: () => {
const view = {...viewConfig, packet_flow_view: !viewConfig["packet_flow_view"]};
setViewConfig(view);
}},
];
const handleClose = (event) => {
if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
return;
}
setDropdownOpen(false);
};
const toggleViewConfig = (field) => {
setViewConfig({...viewConfig, [field]: !viewConfig[field]});
}
const toggleRankDirection = () => {
setViewConfig({...viewConfig, rankDir: viewConfig["rankDir"] === "LR" ? "TB" : "LR"});
}
return (
<div >
<ButtonGroup variant="contained" ref={dropdownAnchorRef} aria-label="split button" style={{marginTop: "10px"}} color="primary">
<Button size="small" color="primary" aria-controls={dropdownOpen ? 'split-button-menu' : undefined}
aria-expanded={dropdownOpen ? 'true' : undefined}
aria-haspopup="menu"
onClick={handleDropdownToggle}>
Configure Graph View <ArrowDropDownIcon />
</Button>
</ButtonGroup>
<div className="mythic-callback-graph-options flex flex-column gap-4">
<Button
size="small"
className="mythic-callback-graph-options-toggle rounded"
onClick={() => setShowConfiguration(!showConfiguration)}
>
{showConfiguration ? "Hide Graph Options" : "Graph Options"}
</Button>
{showConfiguration &&
<FormControl sx={{ width: 200, marginTop: "10px", backgroundColor: theme.palette.background.default}} >
<InputLabel id="demo-chip-label">Group Callbacks By</InputLabel>
<Select
labelId="demo-chip-label"
id="demo-chip"
value={selectedGroupBy}
onChange={handleGroupByChange}
input={<OutlinedInput id="select-chip" label="Group Callbacks By" />}
>
{groupByOptions.map((name) => (
<MenuItem
key={name}
value={name}
<div className="mythic-callback-graph-options-panel flex flex-column gap-5 min-w-0 rounded bg-surface border-subtle">
<div className="mythic-callback-graph-options-actions flex flex-wrap gap-3 min-w-0">
<MythicActionButton
size="small"
className="mythic-callback-graph-option-button"
colorMode={!viewConfig["include_disconnected"] ? "always" : "hover"}
tone={!viewConfig["include_disconnected"] ? "success" : "neutral"}
onClick={() => toggleViewConfig("include_disconnected")}
>
{viewConfig["include_disconnected"] ? "All Edges" : "Active Edges"}
</MythicActionButton>
<MythicActionButton
size="small"
className="mythic-callback-graph-option-button"
colorMode={viewConfig["show_all_nodes"] ? "always" : "hover"}
tone={viewConfig["show_all_nodes"] ? "warning" : "neutral"}
onClick={() => toggleViewConfig("show_all_nodes")}
>
{viewConfig["show_all_nodes"] ? "All Callbacks" : "Connected Callbacks"}
</MythicActionButton>
<Button
size="small"
className="mythic-callback-graph-option-button rounded"
onClick={toggleRankDirection}
>
{viewConfig["rankDir"] === "LR" ? "Left to Right" : "Top to Bottom"}
</Button>
<Button
size="small"
className="mythic-callback-graph-option-button rounded"
onClick={() => toggleViewConfig("packet_flow_view")}
>
{viewConfig["packet_flow_view"] ? "Egress Routes" : "Connection Directions"}
</Button>
</div>
<div className="mythic-callback-graph-options-fields gap-5 min-w-0 grid">
<FormControl size="small" className="mythic-callback-graph-options-field">
<InputLabel id="callback-graph-group-label">Group By</InputLabel>
<Select
labelId="callback-graph-group-label"
id="callback-graph-group"
value={selectedGroupBy}
onChange={handleGroupByChange}
input={<OutlinedInput id="select-callback-graph-group" label="Group By" />}
>
{name}
</MenuItem>
))}
</Select>
</FormControl>
}
{showConfiguration &&
<FormControl sx={{minWidth: 300, marginTop: "10px",backgroundColor: theme.palette.background.default}}>
<InputLabel id="demo-multiple-chip-label">Display Properties per Callback</InputLabel>
<Select
labelId="demo-multiple-chip-label"
id="demo-multiple-chip"
multiple
value={selectedComponentOptions}
onChange={handleChange}
input={<OutlinedInput id="select-multiple-chip" label="Display Properties per Callback" />}
MenuProps={MenuProps}
>
{labelComponentOptions.map((name) => (
<MenuItem
key={name}
value={name}
style={getStyles(name, selectedComponentOptions, theme)}
>
{name}
</MenuItem>
))}
</Select>
</FormControl>
}
<Popper open={dropdownOpen} anchorEl={dropdownAnchorRef.current} transition role={undefined} style={{zIndex: 200}}>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
}}
>
<Paper style={{backgroundColor: theme.palette.mode === 'dark' ? theme.palette.primary.dark : theme.palette.primary.light, color: "white"}}>
<ClickAwayListener onClickAway={handleClose}>
<MenuList id="split-button-menu">
{options.map((option, index) => (
{groupByOptions.map((name) => (
<MenuItem
key={option.name}
onClick={(event) => handleMenuItemClick(event, index)}
key={name}
value={name}
>
{option.name}
{name}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</Select>
</FormControl>
<FormControl size="small" className="mythic-callback-graph-options-field mythic-callback-graph-options-field-wide">
<InputLabel id="callback-graph-label-components-label">Callback Labels</InputLabel>
<Select
labelId="callback-graph-label-components-label"
id="callback-graph-label-components"
multiple
value={selectedComponentOptions}
onChange={handleChange}
input={<OutlinedInput id="select-callback-graph-label-components" label="Callback Labels" />}
MenuProps={MenuProps}
renderValue={(selected) => selected.join(", ")}
>
{labelComponentOptions.map((name) => (
<MenuItem
key={name}
value={name}
style={getStyles(name, selectedComponentOptions, theme)}
>
{name}
</MenuItem>
))}
</Select>
</FormControl>
</div>
{viewConfig["show_all_nodes"] &&
<div className="mythic-callback-graph-options-warning text-xs rounded mythic-tone-warning bg-tone-1 border border-tone-2 text-tone">
Showing all callbacks can be slow in large operations.
</div>
}
</div>
}
</div>
)
}
export function CallbacksGraph({}){
const theme = useTheme();
export function CallbacksGraph({onOpenTab}){
const callbacks = useContext(CallbacksContext);
const callbackgraphedges = useContext(CallbackGraphEdgesContext);
const onOpenTab = useContext(OnOpenTabContext);
//used for creating a task to do a link command
const [loadingSettings, setLoadingSettings] = React.useState(true);
const [filterOptions, setFilterOptions] = React.useState({});
const [linkCommands, setLinkCommands] = React.useState([]);
const [openParametersDialog, setOpenParametersDialog] = React.useState(false);
const [openSelectLinkCommandDialog, setOpenSelectLinkCommandDialog] = React.useState(false);
@@ -257,17 +233,18 @@ export function CallbacksGraph({}){
}
});
const {submitTask, dialog: taskReferenceSubmitDialog} = useTaskReferenceSubmitter(createTask);
const submitParametersDialog = (cmd, parameters, files) => {
setOpenParametersDialog(false);
createTask({variables: {callback_id: selectedCallback.id, command: cmd, params: parameters, files}});
submitTask({variables: {callback_display_id: selectedCallback.display_id, command: cmd, params: parameters, files}});
}
const [viewConfig, setViewConfig] = React.useState({
rankDir: "TB",
label_components: ["display_id", "user"],
packet_flow_view: true,
include_disconnected: true,
include_disconnected: false,
show_all_nodes: false,
group_by: "None"
group_by: "host"
});
const [hideCallback] = useMutation(hideCallbackMutation, {
update: (cache, {data}) => {
@@ -322,20 +299,20 @@ export function CallbacksGraph({}){
}
const contextMenu = useMemo(() => {return [
{
title: 'Hide Callback',
onClick: function(node) {
hideCallback({variables: {callback_display_id: node.display_id}});
}
},
{
title: 'Interact',
title: 'Open Tasking',
onClick: function(node){
console.log(node);
onOpenTab({tabType: "interact", tabID: node.callback_id + "interact", callbackID: node.callback_id});
}
},
{
title: "Manually Remove Edge",
title: "Add P2P Edge",
onClick: function(node){
setAddEdgeSource(node);
setManuallyAddEdgeDialogOpen(true);
}
},
{
title: "Remove Active Edge",
onClick: function(node){
const opts = callbackgraphedges.reduce( (prev, e) => {
if(e.source.id === node.id || e.destination.id === node.id){
@@ -357,15 +334,8 @@ export function CallbacksGraph({}){
setManuallyRemoveEdgeDialogOpen(true);
}
},
{
title: "Manually Add Edge",
onClick: function(node){
setAddEdgeSource(node);
setManuallyAddEdgeDialogOpen(true);
}
},
{
title: "Task Callback for Edge",
title: "Task Link Command",
onClick: function(node){
setLinkCommands([]);
setSelectedLinkCommand(null);
@@ -374,15 +344,48 @@ export function CallbacksGraph({}){
setSelectedCallback(node);
}
},
]}, [getLinkCommands, hideCallback, viewConfig, onOpenTab]);
{
title: 'Hide Callback',
onClick: function(node) {
hideCallback({variables: {callback_display_id: node.display_id}});
}
},
]}, [getLinkCommands, hideCallback, callbackgraphedges, onOpenTab]);
React.useEffect( () => {
try {
const storageItemOptions = GetMythicSetting({setting_name: "callbacks_table_filter_options", default_value: operatorSettingDefaults.callbacks_table_filters});
if(storageItemOptions !== null){
setFilterOptions(storageItemOptions);
}
}catch(error){
console.log("Failed to load callbacks_table_filter_options", error);
}
setLoadingSettings(false);
}, []);
if(loadingSettings){
return (<div style={{width: '100%', height: '100%', position: "relative",}}>
<div style={{overflowY: "hidden", flexGrow: 1}}>
<div style={{
position: "absolute",
left: "35%",
top: "40%"
}}>
{"Loading Callbacks and Connections..."}
</div>
</div>
</div>)
}
return (
<div style={{maxWidth: "100%", "overflow": "hidden", height: "100%"}}>
{taskReferenceSubmitDialog}
{manuallyRemoveEdgeDialogOpen &&
<MythicDialog fullWidth={true} maxWidth="sm" open={manuallyRemoveEdgeDialogOpen}
onClose={()=>{setManuallyRemoveEdgeDialogOpen(false);}}
innerDialog={<MythicSelectFromListDialog onClose={()=>{setManuallyRemoveEdgeDialogOpen(false);}} identifier="id" display="display"
onClose={() => {
setManuallyRemoveEdgeDialogOpen(false);
}}
innerDialog={<MythicSelectFromListDialog onClose={() => {
setManuallyRemoveEdgeDialogOpen(false);}} identifier="id" display="display"
onSubmit={onSubmitManuallyRemoveEdge} options={edgeOptions} title={"Manually Remove Edge"} action={"remove"} />}
/>
}
@@ -396,7 +399,7 @@ export function CallbacksGraph({}){
{openParametersDialog &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openParametersDialog}
onClose={()=>{setOpenParametersDialog(false);}}
innerDialog={<TaskParametersDialog command={selectedLinkCommand} callback={selectedCallback} onSubmit={submitParametersDialog} onClose={()=>{setOpenParametersDialog(false);}} />}
innerDialog={<TaskParametersDialog command={selectedLinkCommand} callback={selectedCallback} callback_display_id={selectedCallback?.display_id} onSubmit={submitParametersDialog} onClose={()=>{setOpenParametersDialog(false);}} />}
/>
}
{openSelectLinkCommandDialog &&
@@ -407,10 +410,9 @@ export function CallbacksGraph({}){
action={"select"} display={"display"} identifier={"display"}/>}
/>
}
<DrawC2PathElementsFlowWithProvider edges={callbackgraphedges} view_config={viewConfig} theme={theme}
panel={<GraphViewOptions setViewConfig={setViewConfig} viewConfig={viewConfig} />}
contextMenu={contextMenu}/>
<DrawC2PathElementsFlowWithProvider providedNodes={callbacks} edges={callbackgraphedges} view_config={viewConfig}
filterOptions={filterOptions}
panel={<GraphViewOptions setViewConfig={setViewConfig} viewConfig={viewConfig} />} contextMenu={contextMenu}/>
</div>
);
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,134 @@
import {MythicActionButton} from "../../MythicComponents/MythicActionButton";
import React from 'react';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import {MythicDraggablePortal, reorder} from "../../MythicComponents/MythicDraggableList";
import {
Draggable,
DragDropContext,
Droppable,
} from "@hello-pangea/dnd";
import DragHandleIcon from '@mui/icons-material/DragHandle';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import VisibilityIcon from '@mui/icons-material/Visibility';
import {
MythicDialogBody,
MythicDialogButton,
MythicDialogFooter,
MythicDialogSection,
} from "../../MythicComponents/MythicDialogLayout";
export function CallbacksTableColumnsReorderDialog({initialItems, onSubmit, onClose, onReset, visible, hidden}) {
const [items, setItems] = React.useState(initialItems);
const onDragEnd = ({ destination, source }) => {
// dropped outside the list
if (!destination) return;
const newItems = reorder(items, source.index, destination.index);
setItems(newItems);
};
React.useEffect( () => {
setItems((currentItems) => currentItems.map( c => {
if(visible.includes(c.name)){
return {...c, visible: true};
}
return {...c, visible: false};
}));
}, [visible, hidden]);
const onToggleVisibility = (i) => {
const newItems = items.map( (c, index) => {
if(index === i){
return {...c, visible: !c.visible};
}
return {...c};
});
setItems(newItems);
}
const onFinish = () => {
onSubmit(items);
}
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Column Layout</DialogTitle>
<DialogContent dividers={true} sx={{p: 0, overflow: "hidden"}}>
<MythicDialogBody sx={{height: "min(70vh, 42rem)", p: 1}}>
<MythicDialogSection
title="Columns"
sx={{display: "flex", flexDirection: "column", flex: "1 1 auto", minHeight: 0}}
>
<DraggableList items={items} onToggleVisibility={onToggleVisibility} onDragEnd={onDragEnd} />
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<MythicDialogFooter>
<MythicDialogButton onClick={onClose}>
Close
</MythicDialogButton>
<MythicDialogButton onClick={onReset} intent="warning">
Reset
</MythicDialogButton>
<MythicDialogButton onClick={onFinish} intent="primary">
Save
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
);
}
export const DraggableList = ({ items, onDragEnd, onToggleVisibility }) => {
return (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="callback-table-column-list">
{(provided) => (
<div className="mythic-reorder-list flex flex-fill flex-column gap-4 min-h-0 overflow-auto" ref={provided.innerRef} {...provided.droppableProps}>
{items.map((item, index) => (
<DraggableListItem onToggleVisibility={onToggleVisibility} item={item} index={index} key={item.key} />
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
};
export const DraggableListItem = ({ item, index, onToggleVisibility }) => {
return (
<Draggable draggableId={item.key} index={index}>
{(provided, snapshot) => {
const row = (
<div
ref={provided.innerRef}
className={`mythic-reorder-row items-center flex flex-none gap-4 rounded bg-surface-raised border-subtle text-primary shadow-none${snapshot.isDragging ? " mythic-reorder-row-dragging mythic-tone-primary bg-tone-1 border-tone-3 shadow-3" : ""}${item.visible ? "" : " mythic-reorder-row-disabled"} min-w-0 w-full`}
{...provided.draggableProps}
>
<span className="mythic-reorder-drag-handle items-center inline-flex justify-center rounded bg-neutral-2 border-subtle text-muted" {...provided.dragHandleProps}>
<DragHandleIcon fontSize="small" />
</span>
<div className="mythic-reorder-row-main items-center flex flex-fill gap-4 min-w-0">
<span className="mythic-reorder-row-title text-sm font-750 leading-125 min-w-0 truncate whitespace-nowrap">{item.name}</span>
</div>
<div className="mythic-reorder-row-actions items-center flex flex-none gap-3">
<MythicActionButton iconOnly
aria-label={item.visible ? `Hide ${item.name}` : `Show ${item.name}`}
appearance="raised" colorMode="hover" tone={item.visible ? "error" : "info"}
size="small"
onClick={() => onToggleVisibility(index)}
>
{item.visible ? (
<VisibilityIcon fontSize="small" />
) : (
<VisibilityOffIcon fontSize="small" />
)}
</MythicActionButton>
</div>
</div>
);
return (
<MythicDraggablePortal isDragging={snapshot.isDragging}>
{row}
</MythicDraggablePortal>
);
}}
</Draggable>
);
};
@@ -0,0 +1,141 @@
import React, {useEffect} from 'react';
import {MythicActionButton} from '../../MythicComponents/MythicActionButton';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import Box from '@mui/material/Box';
import AceEditor from 'react-ace';
import "ace-builds/src-noconflict/mode-json";
import "ace-builds/src-noconflict/theme-github";
import "ace-builds/src-noconflict/theme-monokai";
import "ace-builds/src-noconflict/ext-searchbox";
import {useTheme} from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import {getReadableTextColor, isValidHexColor, MythicColorSwatchInput} from '../../MythicComponents/MythicColorInput';
export function CallbacksTableEditDescriptionColorDialog(props) {
const [comment, setComment] = React.useState("");
const [color, setColor] = React.useState("");
const theme = useTheme();
const onCommitSubmit = () => {
const normalizedColor = color?.toLowerCase() || "";
if(normalizedColor === "" || normalizedColor === "ffffff" || normalizedColor === "#ffffff" ||
normalizedColor === "000000" || normalizedColor === "#000000"){
props.onSubmit(comment, "");
} else {
props.onSubmit(comment, color);
}
if(props.dontCloseOnSubmit){
return;
}
props.onClose();
}
const onChange = (value) => {
setComment(value);
}
useEffect( () => {
try{
setComment(JSON.stringify(JSON.parse(props.description), null, 2));
}catch(error){
setComment(props.description);
}
setColor(props.color);
}, [props.description, props.color]);
const previewColor = isValidHexColor(color) ? color : "";
const darkPreviewBackground = previewColor || "#1f2937";
const lightPreviewBackground = previewColor || "#f8fafc";
const darkPreviewTextColor = previewColor ? getReadableTextColor(previewColor) : "#ffffff";
const lightPreviewTextColor = previewColor ? getReadableTextColor(previewColor) : "#111827";
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">{props.title}</DialogTitle>
<DialogContent dividers={true} style={{height: "100%", margin: 0, padding: 0}}>
<AceEditor
mode="json"
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}
value={comment}
focus={true}
onChange={onChange}
setOptions={{
tabSize: 4,
useWorker: false,
showInvisibles: false,
}}
/>
<Box sx={{p: 1.5, borderTop: "1px solid", borderColor: "divider", backgroundColor: "background.paper"}}>
<Box
sx={{
p: 1.5,
border: "1px solid",
borderColor: "divider",
borderRadius: "8px",
}}
>
<Box sx={{display: "flex", alignItems: "center", justifyContent: "space-between", gap: 2, mb: 1}}>
<Box sx={{minWidth: 0}}>
<Typography variant="body2" sx={{fontWeight: 700}}>Callback Color</Typography>
<Typography variant="caption" sx={{color: "text.secondary"}}>Callback row background and tasking accent</Typography>
</Box>
<Box sx={{display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap", justifyContent: "flex-end"}}>
<MythicColorSwatchInput
color={isValidHexColor(color) ? color : "#000000"}
label="Callback color"
onChange={setColor}
/>
<MythicActionButton colorMode="always" onClick={() => setColor("")} tone="success" variant="outlined" size="small">
Clear
</MythicActionButton>
</Box>
</Box>
<Box sx={{display: "grid", gridTemplateColumns: {xs: "1fr", sm: "1fr 1fr"}, gap: 1}}>
<Box
sx={{
minHeight: 40,
px: 1.5,
display: "flex",
alignItems: "center",
borderRadius: "6px",
border: "1px solid",
borderColor: "divider",
backgroundColor: darkPreviewBackground,
}}
>
<Typography sx={{color: darkPreviewTextColor, fontWeight: 700}}>Dark callback row</Typography>
</Box>
<Box
sx={{
minHeight: 40,
px: 1.5,
display: "flex",
alignItems: "center",
borderRadius: "6px",
border: "1px solid",
borderColor: "divider",
backgroundColor: lightPreviewBackground,
}}
>
<Typography sx={{color: lightPreviewTextColor, fontWeight: 700}}>Light callback row</Typography>
</Box>
</Box>
</Box>
</Box>
</DialogContent>
<DialogActions>
<MythicActionButton colorMode="always" onClick={props.onClose} tone="primary" variant="contained">
Close
</MythicActionButton>
{props.onSubmit &&
<MythicActionButton colorMode="always" onClick={onCommitSubmit} tone="success" variant="contained">
Submit
</MythicActionButton>
}
</DialogActions>
</React.Fragment>
);
}
@@ -0,0 +1,102 @@
import React, {useEffect} from 'react';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import Typography from '@mui/material/Typography';
import NotificationsActiveTwoToneIcon from '@mui/icons-material/NotificationsActiveTwoTone';
import MythicTextField from "../../MythicComponents/MythicTextField";
import {
MythicDialogBody,
MythicDialogButton,
MythicDialogFooter,
MythicDialogSection,
MythicForm,
MythicFormField
} from "../../MythicComponents/MythicDialogLayout";
export function CallbacksTableEditTriggerOnCheckinDialog(props) {
const [comment, setComment] = React.useState(0);
const onChange = (name, value, error) => {
if(isNaN(parseInt(value))){
setComment(0);
} else {
setComment(parseInt(value));
}
}
useEffect( () => {
setComment(props.trigger_on_checkin_after_time);
}, [props.trigger_on_checkin_after_time]);
const onSubmit = () => {
props.onSubmit(comment);
}
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Callback check-in alert</DialogTitle>
<DialogContent dividers={true}>
<MythicDialogBody>
<div className="mythic-callback-trigger-summary p-6 items-start flex gap-6 min-w-0 rounded bg-surface-muted border-subtle">
<span className={`mythic-callback-trigger-summary-icon items-center inline-flex flex-none justify-center rounded bg-neutral-1 border-subtle text-muted${comment > 0 ? " mythic-callback-trigger-summary-icon-active" : ""}`}>
<NotificationsActiveTwoToneIcon fontSize="small" />
</span>
<div className="mythic-callback-trigger-summary-copy min-w-0">
<Typography className="mythic-callback-trigger-summary-title">
{comment > 0 ? `Alert after ${comment} minute${comment === 1 ? "" : "s"} without a check-in` : "Alerting disabled"}
</Typography>
<Typography className="mythic-callback-trigger-summary-description">
This setting only triggers when the callback checks in after crossing the configured threshold.
</Typography>
</div>
</div>
<MythicDialogSection
title="Threshold"
description="Set how many minutes this callback can remain silent before its next check-in can trigger eventing."
>
<MythicForm>
<MythicFormField
label="Minutes without a check-in"
description="Use 0 to disable this alert for the callback."
>
<div className="mythic-form-field-control min-w-0 w-full">
<MythicTextField
autoFocus={true}
onChange={onChange}
type="number"
value={comment}
onEnter={onSubmit}
name="Trigger threshold"
showLabel={false}
marginTop="0"
marginBottom="0"
InputProps={{inputProps: {min: 0}}}
/>
</div>
</MythicFormField>
</MythicForm>
</MythicDialogSection>
<MythicDialogSection title="Eventing behavior">
<div className="mythic-callback-trigger-rule-list gap-4 grid">
<div className="mythic-callback-trigger-rule text-xs leading-140 rounded bg-surface-muted border-subtle text-muted">
Trigger name: <strong>callback_checkin</strong>
</div>
<div className="mythic-callback-trigger-rule text-xs leading-140 rounded bg-surface-muted border-subtle text-muted">
Matching workflow filters still apply, including payload type and supported OS restrictions.
</div>
<div className="mythic-callback-trigger-rule text-xs leading-140 rounded bg-surface-muted border-subtle text-muted">
If no matching workflow exists, no workflow will run.
</div>
</div>
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</MythicDialogButton>
{props.onSubmit &&
<MythicDialogButton onClick={onSubmit} intent="primary">
Save Threshold
</MythicDialogButton>
}
</MythicDialogFooter>
</React.Fragment>
);
}
@@ -1,63 +1,41 @@
import React, {useCallback, useEffect, useContext} from 'react';
import {Button} from '@mui/material';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
import { MythicDisplayTextDialog} from '../../MythicComponents/MythicDisplayTextDialog';
import {MythicModifyStringDialog} from '../../MythicComponents/MythicDialog';
import ButtonGroup from '@mui/material/ButtonGroup';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import KeyboardIcon from '@mui/icons-material/Keyboard';
import LockIcon from '@mui/icons-material/Lock';
import WifiIcon from '@mui/icons-material/Wifi';
import InsertLinkTwoToneIcon from '@mui/icons-material/InsertLinkTwoTone';
import {C2PathDialog} from './C2PathDialog';
import {snackActions} from '../../utilities/Snackbar';
import Paper from '@mui/material/Paper';
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 VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import {
hideCallbackMutation,
lockCallbackMutation,
unlockCallbackMutation,
updateIPsCallbackMutation
updateIPsCallbackMutation,
} from './CallbackMutations';
import {useMutation } from '@apollo/client';
import SnoozeIcon from '@mui/icons-material/Snooze';
import AccountTreeIcon from '@mui/icons-material/AccountTree';
import {useTheme} from '@mui/material/styles';
import LockOpenIcon from '@mui/icons-material/LockOpen';
import EditIcon from '@mui/icons-material/Edit';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {faQuestion, faSkullCrossbones, faFolderOpen, faList} from '@fortawesome/free-solid-svg-icons';
import {faQuestion, faRobot} from '@fortawesome/free-solid-svg-icons';
import {faLinux, faApple, faWindows, faChrome, faAndroid} from '@fortawesome/free-brands-svg-icons';
import {DetailedCallbackTable} from './DetailedCallbackTable';
import InfoIcon from '@mui/icons-material/Info';
import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip';
import {TaskFromUIButton} from './TaskFromUIButton';
import { MythicSelectFromRawListDialog } from '../../MythicComponents/MythicSelectFromListDialog';
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
import { areEqual } from 'react-window';
import {CallbackGraphEdgesContext, OnOpenTabContext} from './CallbacksTop';
import Moment from 'react-moment';
import moment from 'moment';
import WidgetsIcon from '@mui/icons-material/Widgets';
import {ModifyCallbackMythicTreeGroupsDialog} from "./ModifyCallbackMythicTreeGroupsDialog";
import {GetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
import TerminalIcon from '@mui/icons-material/Terminal';
import VerticalSplitIcon from '@mui/icons-material/VerticalSplit';
import {faSocks} from '@fortawesome/free-solid-svg-icons';
import {TagsDisplay, ViewEditTags} from "../../MythicComponents/MythicTag";
import {MythicActionButton} from "../../MythicComponents/MythicActionButton";
import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip";
import {MythicTimestamp} from "../../MythicComponents/MythicTimestamp";
import ButtonGroup from "@mui/material/ButtonGroup";
import Button from "@mui/material/Button";
export const CallbacksTableIDCell = React.memo(({rowData, toggleLock, updateDescription, setOpenHideMultipleDialog, setOpenTaskMultipleDialog}) =>{
export const CallbacksTableIDCell = React.memo(({rowData, callbackDropdown, onOpenTriggerDialog, onOpenUnlockDialog}) =>{
const dropdownAnchorRef = React.useRef(null);
const theme = useTheme();
const onOpenTab = useContext(OnOpenTabContext);
const [openMetaDialog, setOpenMetaDialog] = React.useState(false);
const [dropdownOpen, setDropdownOpen] = React.useState(false);
const [openEditDescriptionDialog, setOpenEditDescriptionDialog] = React.useState(false);
const [openTaskingButton, setOpenTaskingButton] = React.useState(false);
const [openEditMythicTreeGroupsDialog, setOpenEditMythicTreeGroupsDialog] = React.useState(false);
const taskingData = React.useRef({"parameters": "", "ui_feature": "callback_table:exit"});
const interactType = GetMythicSetting({setting_name: "interactType", default_value: "interact"})
const [rowDataStatic, setRowDataStatic] = React.useState(rowData);
React.useEffect( () => {
let update = false;
@@ -76,242 +54,130 @@ export const CallbacksTableIDCell = React.memo(({rowData, toggleLock, updateDesc
if(rowData.description !== rowDataStatic.description){
update = true;
}
if(rowData.id !== rowDataStatic.id){
update = true;
}
if(rowData.color !== rowDataStatic.color){
update = true;
}
if(rowData.trigger_on_checkin_after_time !== rowDataStatic.trigger_on_checkin_after_time){
update = true;
}
if(rowData.callbackports.length !== rowDataStatic.callbackports.length){
update = true;
}
if(update){
setRowDataStatic(rowData);
}
}, [rowData]);
const [lockCallback] = useMutation(lockCallbackMutation, {
update: (cache, {data}) => {
if(data.updateCallback.status === "success"){
snackActions.success("Locked callback");
}else{
snackActions.warning(data.updateCallback.error);
const proxyMessage = useCallback(() => {
let message = [];
for(let i = 0; i < rowDataStatic?.callbackports?.length || 0; i++){
switch(rowDataStatic.callbackports[i].port_type){
case "socks":
if(rowDataStatic.callbackports[i].username === ""){
message.push(`socks: ${rowDataStatic.callbackports[i].local_port}`);
} else {
message.push(`socks: ${rowDataStatic.callbackports[i].local_port}, ${rowDataStatic.callbackports[i].username}:${rowDataStatic.callbackports[i].password}`);
}
break;
case "rpfwd":
if(rowDataStatic.callbackports[i].username === ""){
message.push(`rpfwd: ${rowDataStatic.callbackports[i].local_port}->${rowDataStatic.callbackports[i].remote_ip}:${rowDataStatic.callbackports[i].remote_port}`);
} else {
message.push(`pfwd: ${rowDataStatic.callbackports[i].local_port}->${rowDataStatic.callbackports[i].remote_ip}:${rowDataStatic.callbackports[i].remote_port}, ${rowDataStatic.callbackports[i].username}:${rowDataStatic.callbackports[i].password}`);
}
break;
case "interactive":
message.push(`interactive: ${rowDataStatic.callbackports[i].local_port}`);
break;
}
},
onError: data => {
console.log(data);
snackActions.warning(data);
}
});
const [unlockCallback] = useMutation(unlockCallbackMutation, {
update: (cache, {data}) => {
if(data.updateCallback.status === "success"){
snackActions.success("Unlocked callback");
}else{
snackActions.warning(data.updateCallback.error);
}
},
onError: data => {
console.log(data);
snackActions.warning(data);
}
});
const editDescriptionSubmit = (description) => {
if(description === ""){
updateDescription({description: rowDataStatic.payload.description, callback_display_id: rowDataStatic.display_id});
} else {
updateDescription({description, callback_display_id: rowDataStatic.display_id});
}
}
const handleDropdownToggle = (evt) => {
evt.stopPropagation();
setDropdownOpen((prevOpen) => !prevOpen);
};
const localOnOpenTab = (tabType) => {
if(tabType === "interact"){
onOpenTab({tabType, tabID: rowDataStatic.id + tabType, callbackID: rowDataStatic.id, displayID: rowDataStatic.display_id});
}else if(tabType === "processBrowser"){
onOpenTab({tabType, tabID: rowDataStatic.host, callbackID: rowDataStatic.id, displayID: rowDataStatic.display_id});
}else{
onOpenTab({tabType, tabID: rowDataStatic.id + tabType, callbackID: rowDataStatic.id, displayID: rowDataStatic.display_id});
}
}
const handleMenuItemClick = (event, index) => {
options[index].click(event);
setDropdownOpen(false);
return message.join("\n");
}, [rowDataStatic]);
const handleDropdownToggle = (event) => {
event.stopPropagation();
callbackDropdown({rowDataStatic, event});
};
const handleClose = (event) => {
if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
return;
}
setDropdownOpen(false);
};
const [hideCallback] = useMutation(hideCallbackMutation, {
update: (cache, {data}) => {
if(data.updateCallback.status === "success"){
snackActions.success("Hiding callback");
}else{
snackActions.warning(data.updateCallback.error);
}
},
onError: data => {
console.log(data);
}
});
const localToggleLock = () => {
if(rowDataStatic.locked){
unlockCallback({variables: {callback_display_id: rowDataStatic.display_id}})
}else{
lockCallback({variables: {callback_display_id: rowDataStatic.display_id}})
}
const handleOpenTriggerDialog = (event) => {
event.stopPropagation();
onOpenTriggerDialog?.(rowDataStatic);
};
const handleOpenUnlockDialog = (event) => {
event.stopPropagation();
onOpenUnlockDialog?.(rowDataStatic);
};
const localOnOpenTab = () => {
onOpenTab({tabType: interactType, tabID: rowDataStatic.id + interactType, callbackID: rowDataStatic.id, displayID: rowDataStatic.display_id});
}
let defaultInteractIcon = <KeyboardIcon fontSize="small" />;
if(interactType === "interactSplit"){
defaultInteractIcon = <VerticalSplitIcon fontSize="small" />;
} else if(interactType === "interactConsole"){
defaultInteractIcon = <TerminalIcon fontSize="small" />;
}
const highIntegrity = rowDataStatic.integrity_level >= 3;
const lockOwner = rowDataStatic.locked_operator?.username || "unknown operator";
const onOpenTabClick = (e) => {
e.stopPropagation();
localOnOpenTab()
}
const options = [
{name: 'Hide Callback', icon: <VisibilityOffIcon style={{color: theme.palette.warning.main, paddingRight: "5px"}}/>, click: (evt) => {
evt.stopPropagation();
hideCallback({variables: {callback_display_id: rowDataStatic.display_id}});
}},
{
name: "Hide Multiple", icon: <VisibilityOffIcon style={{paddingRight: "5px"}}/>, click: (evt) => {
setOpenHideMultipleDialog(true);
}
},
{
name: "Exit Callback", icon: <FontAwesomeIcon icon={faSkullCrossbones} style={{color: theme.errorOnMain, cursor: "pointer", marginRight: "10px"}} />, click: (evt) => {
taskingData.current = {"parameters": "", "ui_feature": "callback_table:exit", "getConfirmation": true, acceptText: "exit"};
setOpenTaskingButton(true);
}
},
{
name: "Task Multiple", icon: <FontAwesomeIcon icon={faList} style={{cursor: "pointer", marginRight: "10px"}} />, click: (evt) => {
setOpenTaskMultipleDialog({open: true, data: rowDataStatic});
}
},
{name: 'File Browser', icon: <FontAwesomeIcon icon={faFolderOpen} style={{color: theme.folderColor, cursor: "pointer", marginRight: "10px"}} />, click: (evt) => {
evt.stopPropagation();
localOnOpenTab("fileBrowser");
}},
{name: 'Process Browser', icon: <AccountTreeIcon style={{paddingRight: "5px"}}/>, click: (evt) => {
evt.stopPropagation();
localOnOpenTab("processBrowser");
}},
{name: 'Split Tasking', icon: <KeyboardIcon style={{paddingRight: "5px"}}/>, click: (evt) => {
evt.stopPropagation();
localOnOpenTab("interactSplit");
}},
{name: "Console View", icon: <TerminalIcon style={{paddingRight: "5px"}}/>, click: (evt) => {
evt.stopPropagation();
localOnOpenTab("interactConsole");
}
},
{name: rowDataStatic.locked ? 'Unlock (Locked by ' + rowDataStatic.locked_operator.username + ')' : 'Lock Callback', icon: rowDataStatic.locked ? (<LockOpenIcon style={{color: theme.successOnMain, paddingRight: "5px"}}/>) : (<LockIcon style={{color: theme.errorOnMain, paddingRight: "5px"}} />), click: (evt) => {
evt.stopPropagation();
localToggleLock();
}},
{name: "Edit Description", icon: <EditIcon style={{paddingRight: "5px"}} />, click: (evt) => {
evt.stopPropagation();
setOpenEditDescriptionDialog(true);
}},
{name: "Expand Callback", icon: <OpenInNewIcon style={{paddingRight: "5px"}} />, click: (evt) => {
evt.stopPropagation();
window.open("/new/callbacks/" + rowDataStatic.display_id, "_blank").focus();
}},
{name: "View Metadata", icon: <InfoIcon style={{color: theme.infoOnMain, paddingRight: "5px"}} />, click: (evt) => {
evt.stopPropagation();
setOpenMetaDialog(true);
}},
{name: "Modify Groupings", icon: <WidgetsIcon style={{paddingRight: "5px"}} />, click: (evt) => {
evt.stopPropagation();
setOpenEditMythicTreeGroupsDialog(true);
}}
];
return (
<div>
<ButtonGroup
color={rowDataStatic.integrity_level > 2 ? "error" : "primary"}
ref={dropdownAnchorRef}
aria-label="split button"
>
<Button style={{padding: "0 10px 0 10px"}} color={rowDataStatic.integrity_level > 2 ? "error" : "primary"} variant="contained"
onClick={(evt) => {evt.stopPropagation();localOnOpenTab("interact")}}>
{ rowDataStatic.locked ? (<LockIcon fontSize="large" style={{color: theme.errorOnMain, marginRight: "10px"}} />):(<KeyboardIcon fontSize="large" style={{marginRight: "10px"}}/>) }
{rowDataStatic.display_id}
</Button>
<Button
style={{margin: 0, padding: 0}}
variant="contained"
aria-controls={dropdownOpen ? 'split-button-menu' : undefined}
aria-expanded={dropdownOpen ? 'true' : undefined}
color={rowDataStatic.integrity_level > 2 ? "error" : "primary"}
<div id={`callbacksTableID${rowDataStatic.id}`} className="mythic-callback-interactCell items-center inline-flex gap-1 overflow-hidden w-full">
<ButtonGroup className="mythic-split-action-group inline-flex " >
<MythicActionButton
appearance={"raised"}
compact
colorMode={highIntegrity ? "always" : "hover"}
icon={defaultInteractIcon}
onClick={onOpenTabClick}
tone={highIntegrity ? "error" : "primary"}
tooltip={`Open ${interactType === "interactSplit" ? "split" : interactType === "interactConsole" ? "console" : "tasking"} view${highIntegrity ? " - high integrity callback" : ""}`}
>
<span className="font-700 font-mono text-output">
{rowDataStatic.display_id}
</span>
</MythicActionButton>
<MythicActionButton
appearance={"raised"}
compact
aria-haspopup="menu"
icon={<ArrowDropDownIcon />}
onClick={handleDropdownToggle}
>
<ArrowDropDownIcon/>
</Button>
ref={dropdownAnchorRef}
colorMode={highIntegrity ? "always" : "hover"}
tone={highIntegrity ? "error" : "primary"}
/>
</ButtonGroup>
<Popper open={dropdownOpen} anchorEl={dropdownAnchorRef.current} role={undefined} transition style={{zIndex: 200}}>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement === 'bottom' ? 'top left' : 'top center',
}}
>
<Paper className={"dropdownMenuColored"} elevation={5}>
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
<MenuList id="split-button-menu">
{options.map((option, index) => (
<MenuItem
key={option.name}
onClick={(event) => handleMenuItemClick(event, index)}
>
{option.icon}{option.name}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
{openTaskingButton &&
<TaskFromUIButton ui_feature={taskingData.current?.ui_feature || " "}
callback_id={rowDataStatic.id}
display_id={rowDataStatic.display_id}
parameters={taskingData.current?.parameters || ""}
openDialog={taskingData.current?.openDialog || false}
getConfirmation={taskingData.current?.getConfirmation || false}
acceptText={taskingData.current?.acceptText || "YES"}
onTasked={() => setOpenTaskingButton(false)}/>
}
{openMetaDialog &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openMetaDialog}
onClose={()=>{setOpenMetaDialog(false);}}
innerDialog={<DetailedCallbackTable onClose={()=>{setOpenMetaDialog(false);}} callback_id={rowDataStatic.id} />}
/>
{rowDataStatic.locked &&
<MythicStyledTooltip title={`Locked by ${lockOwner}`}>
<MythicStatusChip
aria-label={`Unlock callback ${rowDataStatic.display_id}`}
compact
iconOnly
onClick={handleOpenUnlockDialog}
status="locked"
/>
</MythicStyledTooltip>
}
{openEditDescriptionDialog &&
<MythicDialog
fullWidth={true}
open={openEditDescriptionDialog}
onClose={() => {setOpenEditDescriptionDialog(false);}}
innerDialog={
<MythicModifyStringDialog title={"Edit Callback's Description"}
onClose={() => {setOpenEditDescriptionDialog(false);}}
value={rowDataStatic.description}
onSubmit={editDescriptionSubmit}
/>
}
/>
{rowDataStatic.trigger_on_checkin_after_time > 0 &&
<MythicStyledTooltip title={`Alert on callback after no checkin for ${rowDataStatic.trigger_on_checkin_after_time} minutes`}>
<MythicStatusChip
aria-label={`Adjust alert trigger for callback ${rowDataStatic.display_id}`}
compact
iconOnly
onClick={handleOpenTriggerDialog}
status="alerting"
/>
</MythicStyledTooltip>
}
{openEditMythicTreeGroupsDialog &&
<MythicDialog
fullWidth={true}
maxWidth={"lg"}
open={openEditMythicTreeGroupsDialog}
onClose={() => {setOpenEditMythicTreeGroupsDialog(false);}}
innerDialog={
<ModifyCallbackMythicTreeGroupsDialog callback_id={rowDataStatic.id}
onClose={() => {setOpenEditMythicTreeGroupsDialog(false);}} />
}
/>
{rowDataStatic.callbackports.length > 0 &&
<MythicStyledTooltip title={proxyMessage()}>
<MythicStatusChip compact icon={<FontAwesomeIcon icon={faSocks} />} iconOnly label="Proxy active" />
</MythicStyledTooltip>
}
</div>
</div>
)
},
areEqual)
@@ -320,39 +186,49 @@ export const CallbacksTableStringCell = React.memo(({rowData, cellData}) => {
<div>{cellData}</div>
)
}, areEqual)
export const CallbacksTableLastCheckinCell = React.memo( ({rowData, cellData}) => {
const adjustOutput = (newTime) => {
if(newTime.includes("54 years")){
return "Streaming Now"
}else if(newTime === "a few seconds"){
moment.relativeTimeThreshold('s', 60);
moment.relativeTimeThreshold('ss', 0);
return moment(rowData.last_checkin + "Z", "YYYY-MM-DDTHH:mm:ss.SSSSSSZ").fromNow(true)
}
return newTime;
export const CallbacksTableLastCheckinCell = React.memo( ({rowData, cellData, me}) => {
if(rowData?.payload?.payloadtype?.agent_type !== "agent"){
return ""
}
if(rowData.last_checkin === "1970-01-01T00:00:00"){
return (
<div className="mythic-callback-cellInline items-center inline-flex gap-2 h-full max-w-full min-w-0 overflow-hidden w-full">
{rowData.dead &&
<MythicStyledTooltip title={"Based on callback's last checkin and sleep info, it's likely dead"}>
<MythicStatusChip compact iconOnly status="dead" />
</MythicStyledTooltip>
}
<span className="mythic-callback-cellText min-w-0 truncate whitespace-nowrap">Streaming Now</span>
</div>
)
}
return (
<div>
<Moment filter={adjustOutput} interval={500} parse={"YYYY-MM-DDTHH:mm:ss.SSSSSSZ"}
<div className="mythic-callback-cellInline items-center inline-flex gap-2 h-full max-w-full min-w-0 overflow-hidden w-full">
{rowData.dead &&
<MythicStyledTooltip title={"Based on callback's last checkin and sleep info, it's likely dead"}>
<MythicStatusChip compact iconOnly status="dead" />
</MythicStyledTooltip>
}
<span className="mythic-callback-cellText min-w-0 truncate whitespace-nowrap">
<MythicTimestamp
interval={1000}
withTitle
titleFormat={"YYYY-MM-DD HH:mm:ss"}
fromNow ago
serverSkew={me?.user?.server_skew}
fromNow
ago
>
{rowData.last_checkin + "Z"}
</Moment>
</MythicTimestamp>
</span>
</div>
)
});
}, areEqual);
export const CallbacksTablePayloadTypeCell = React.memo( ({rowData}) => {
const payloadTypeName = React.useRef(rowData.payload.payloadtype.name)
return (
<MythicStyledTooltip title={payloadTypeName.current}>
<img
style={{width: "35px", height: "35px"}}
src={"/static/" + payloadTypeName.current + ".svg"}
/>
</MythicStyledTooltip>
rowData?.payload?.payloadtype?.name
)
}, areEqual)
export const CallbacksTableIPCell = React.memo(({cellData, rowData}) => {
@@ -376,7 +252,8 @@ export const CallbacksTableIPCell = React.memo(({cellData, rowData}) => {
const updateIPsInfo = React.useCallback( ({callback_display_id, ips}) => {
updateIPs({variables: {callback_display_id: callback_display_id, ips}})
}, []);
const onClick = () => {
const onClick = (event) => {
event.stopPropagation();
setOpenPickIP(true);
}
const editIPSubmit = (selected_ip) => {
@@ -396,16 +273,14 @@ export const CallbacksTableIPCell = React.memo(({cellData, rowData}) => {
}, [cellData]);
return (
<>
<div style={{display: "flex", alignItems: "center"}}>
<div className="mythic-callback-cellInline items-center inline-flex gap-2 h-full max-w-full min-w-0 overflow-hidden w-full">
{options.length > 1 &&
<MythicStyledTooltip title={"Adjust Displayed"}>
<UnfoldMoreIcon onClick={onClick} style={{paddingTop: "5px", cursor: "pointer"}} />
</MythicStyledTooltip>
<MythicActionButton appearance="raised" compact icon={<UnfoldMoreIcon />} iconOnly onClick={onClick} tooltip="Adjust displayed IP" />
}
{displayIP}
<span className="mythic-callback-cellText min-w-0 truncate whitespace-nowrap">{displayIP}</span>
</div>
{openPickIP &&
<MythicDialog fullWidth={true} open={openPickIP} onClose={() => {setOpenPickIP(false);}}
<MythicDialog fullWidth={true} open={openPickIP} onClose={() => {setOpenPickIP(false);}}
innerDialog={
<MythicSelectFromRawListDialog
onClose={() => {setOpenPickIP(false);}}
@@ -419,13 +294,10 @@ export const CallbacksTableIPCell = React.memo(({cellData, rowData}) => {
)
}, areEqual)
export const CallbacksTableC2Cell = React.memo(({rowData}) => {
const theme = useTheme();
export const CallbacksTableC2Cell = React.memo(({rowData, centered = true}) => {
const [localRowData, setLocalRowData] = React.useState(rowData);
const initialCallbackGraphEdges = useContext(CallbackGraphEdgesContext);
const onOpenTab = useContext(OnOpenTabContext);
const [activeEgress, setActiveEgress] = React.useState(theme.palette.success.main);
const [hasEgressRoute, setHasEgressRoute] = React.useState(true);
const [openC2Dialog, setOpenC2Dialog] = React.useState(false);
const [callbackgraphedges, setCallbackgraphedges] = React.useState([]);
const [callbackgraphedgesAll, setCallbackgraphedgesAll] = React.useState([]);
@@ -433,19 +305,6 @@ export const CallbacksTableC2Cell = React.memo(({rowData}) => {
event.stopPropagation();
setOpenC2Dialog(true);
}
useEffect( () => {
const routes = callbackgraphedgesAll.filter( (edge) => {
if(!edge.c2profile.is_p2p && edge.source.id === localRowData.id && edge.destination.id === localRowData.id){
return true;
}
return false;
}).length;
if(routes > 0 && !hasEgressRoute){
setHasEgressRoute(true);
}else if(routes === 0 && hasEgressRoute){
setHasEgressRoute(false);
}
}, [callbackgraphedgesAll]);
useEffect( () => {
const getEdges = (activeOnly) => {
//update our aggregate of callbackgraphedges for both src and dst that involve us
@@ -466,10 +325,13 @@ export const CallbacksTableC2Cell = React.memo(({rowData}) => {
//look at all of the edges in myEdges and see if there are any edges that share a source/destination in props.callbackgraphedges that are _not_ in myEdges so far
const newEdges = initialCallbackGraphEdges?.reduce( (prev, edge) => {
//looking to see if we should add 'edge' to our list of relevant edges
if(prev.includes(edge)){return [...prev]}
const alreadyIncludes = prev.filter( (e) => e.id === edge.id);
if(alreadyIncludes.length > 0){
return [...prev]
}
//look through all of the previous edges we know about and see if there's a matching source/destination id with the new edge
const matching = prev.filter( (e) => {
if(e.source.id === edge.source.id || e.source.id === edge.destination.id || e.destination.id === edge.source.id ){
if(e.source.id === edge.source.id || e.source.id === edge.destination.id || e.destination.id === edge.source.id || e.destination.id === edge.destination.id ){
if(activeOnly){
if(edge.end_timestamp === null) { return true}
else{return false}
@@ -479,12 +341,12 @@ export const CallbacksTableC2Cell = React.memo(({rowData}) => {
return false;
});
if(matching.length > 0){
return [...prev, edge];
return [...prev, {...edge}];
}else{
return [...prev];
}
}, [...myEdges]) || [];
foundMore = newEdges.length > myEdges;
foundMore = newEdges.length > myEdges.length;
myEdges = [...newEdges];
}
return myEdges;
@@ -494,27 +356,40 @@ export const CallbacksTableC2Cell = React.memo(({rowData}) => {
setCallbackgraphedges(myActiveEdges);
setCallbackgraphedgesAll(myEdges);
}, [initialCallbackGraphEdges]);
}, [initialCallbackGraphEdges, localRowData]);
useEffect( () => {
//determine if there are any active routes left at all
const activeRoutes = callbackgraphedges.filter( (edge) => {
if(!edge.c2profile.is_p2p && edge.end_timestamp === null){
return true;
}
return false
});
if(activeRoutes.length === 0){
setActiveEgress(theme.palette.error.main);
}else{
setActiveEgress(theme.palette.success.main);
if(rowData.id !== localRowData.id){
setLocalRowData(rowData);
}
}, [callbackgraphedges, theme.palette.success.main, theme.palette.error.main]);
}, [rowData]);
if(rowData?.payload?.payloadtype?.agent_type !== "agent"){
return null
}
const directEgressRoutes = callbackgraphedgesAll.filter((edge) =>
!edge.c2profile?.is_p2p &&
edge.source.id === localRowData.id &&
edge.destination.id === localRowData.id
);
const hasDirectEgressRoute = directEgressRoutes.length > 0;
const directEgressActive = directEgressRoutes.some((edge) => edge.end_timestamp === null);
const p2pRouteActive = callbackgraphedges.some((edge) =>
!edge.c2profile?.is_p2p && edge.end_timestamp === null
);
const c2RouteActive = hasDirectEgressRoute ? directEgressActive : p2pRouteActive;
const c2Tooltip = hasDirectEgressRoute ?
(directEgressActive ? "Direct C2 route active. View C2 path information" : "Direct C2 route inactive. View C2 path information") :
(p2pRouteActive ? "Active P2P route to Mythic. View C2 path information" : "No active route to Mythic. View C2 path information");
return (
<div>
{hasEgressRoute ?
<WifiIcon onClick={onOpenC2Dialog} style={{color: activeEgress, cursor: "pointer"}}/> :
<InsertLinkTwoToneIcon onClick={onOpenC2Dialog} style={{color: activeEgress, cursor: "pointer"}} />
}
<div className={`mythic-callback-cellInline items-center inline-flex gap-2${centered ? " mythic-callback-cellInlineCenter justify-center" : ""} h-full max-w-full min-w-0 overflow-hidden w-full`}>
<MythicActionButton appearance="raised"
active={!c2RouteActive}
compact
icon={hasDirectEgressRoute ? <WifiIcon /> : <InsertLinkTwoToneIcon />}
iconOnly
onClick={onOpenC2Dialog}
tone={c2RouteActive ? "success" : "error"}
tooltip={c2Tooltip}
/>
{openC2Dialog &&
<MythicDialog
fullWidth={true}
@@ -523,7 +398,8 @@ export const CallbacksTableC2Cell = React.memo(({rowData}) => {
onClose={()=>{setOpenC2Dialog(false);}}
innerDialog={
<C2PathDialog
onClose={()=>{setOpenC2Dialog(false);}}
onClose={()=>{setOpenC2Dialog(false);}}
callback={localRowData}
callbackgraphedges={callbackgraphedgesAll}
onOpenTab={onOpenTab}
@@ -539,32 +415,36 @@ export const CallbacksTableC2Cell = React.memo(({rowData}) => {
export const CallbacksTableOSCell = React.memo( ({rowData, cellData}) => {
const [openOSDialog, setOpenOSDialog] = React.useState(false);
const getOSIcon = useCallback( () => {
if(rowData?.payload?.payloadtype?.agent_type !== "agent"){
return <FontAwesomeIcon icon={faRobot} />
}
switch(rowData.payload.os.toLowerCase()){
case "windows":
return <FontAwesomeIcon icon={faWindows} size="2x" style={{cursor: "pointer"}} onClick={displayOSInfo} />
return <FontAwesomeIcon icon={faWindows} />
case "linux":
case "centos":
case "redhat":
case "debian":
case "fedora":
case "freebsd":
return <FontAwesomeIcon icon={faLinux} size="2x" style={{cursor: "pointer"}} onClick={displayOSInfo} />
return <FontAwesomeIcon icon={faLinux} />
case "macos":
return <FontAwesomeIcon icon={faApple} size="2x" style={{cursor: "pointer"}} onClick={displayOSInfo}/>
return <FontAwesomeIcon icon={faApple} />
case "chrome":
return <FontAwesomeIcon icon={faChrome} size="2x" style={{cursor: "pointer"}} onClick={displayOSInfo} />
return <FontAwesomeIcon icon={faChrome} />
case "android":
return <FontAwesomeIcon icon={faAndroid} size="2x" style={{cursor: "pointer"}} onClick={displayOSInfo} />
return <FontAwesomeIcon icon={faAndroid} />
default:
return <FontAwesomeIcon icon={faQuestion} size="2x" style={{cursor: "pointer"}} onClick={displayOSInfo} />
return <FontAwesomeIcon icon={faQuestion} />
}
}, []);
const displayOSInfo = React.useCallback( () => {
}, [rowData?.payload?.os]);
const displayOSInfo = React.useCallback( (event) => {
event.stopPropagation();
setOpenOSDialog(true);
}, []);
return (
<div>
{getOSIcon()}
<div className="mythic-callback-cellInline items-center inline-flex gap-2 mythic-callback-cellInlineCenter justify-center h-full max-w-full min-w-0 overflow-hidden w-full">
<MythicActionButton appearance="raised" compact icon={getOSIcon()} iconOnly onClick={displayOSInfo} tone="primary" tooltip="View operating system information" />
{ openOSDialog &&
<MythicDisplayTextDialog
onClose={()=>{setOpenOSDialog(false);}}
@@ -579,9 +459,8 @@ export const CallbacksTableOSCell = React.memo( ({rowData, cellData}) => {
)
});
export const CallbacksTableSleepCell = React.memo( ({rowData, cellData, updateSleepInfo}) => {
const theme = useTheme();
}, areEqual);
export const CallbacksTableSleepCell = React.memo( ({rowData, cellData, updateSleepInfo, centered = true}) => {
const [openSleepDialog, setOpenSleepDialog] = React.useState(false);
const editSleepSubmit = (sleep) => {
updateSleepInfo({sleep_info: sleep, callback_display_id: rowData.display_id});
@@ -590,16 +469,28 @@ export const CallbacksTableSleepCell = React.memo( ({rowData, cellData, updateSl
event.stopPropagation();
setOpenSleepDialog(true);
}
if(rowData?.payload?.payloadtype?.agent_type !== "agent"){
return null
}
return (
<div>
<SnoozeIcon onClick={onOpenSleepDialog}
style={{color: cellData === "" ? theme.palette.warning.main : theme.palette.info.main, cursor: "pointer"}}
<div className={`mythic-callback-cellInline items-center inline-flex gap-2${centered ? " mythic-callback-cellInlineCenter justify-center" : ""} h-full max-w-full min-w-0 overflow-hidden w-full`}>
<MythicActionButton appearance="raised"
active={cellData === ""}
compact
icon={<SnoozeIcon />}
iconOnly
onClick={onOpenSleepDialog}
tone={cellData === "" ? "warning" : "info"}
tooltip={cellData === "" ? "No sleep information set" : "View or edit sleep information"}
/>
{ openSleepDialog &&
<MythicDialog fullWidth={true} open={openSleepDialog} onClose={() => {setOpenSleepDialog(false);}}
<MythicDialog fullWidth={true} open={openSleepDialog} maxWidth={"md"}
style={{height: "100%"}}
onClose={() => {setOpenSleepDialog(false);}}
innerDialog={
<MythicModifyStringDialog title={"View Sleep Information"} multiline maxRows={10}
onClose={() => {setOpenSleepDialog(false);}}
<MythicModifyStringDialog title={"View / Edit Sleep Information - This doesn't issue tasks to the agent, but helps alive/dead tracking"}
maxRows={20}
onClose={() => {setOpenSleepDialog(false);}}
value={cellData}
onSubmit={editSleepSubmit} />
}
@@ -609,4 +500,17 @@ export const CallbacksTableSleepCell = React.memo( ({rowData, cellData, updateSl
)
}, areEqual)
}, areEqual)
export const CallbacksTableTagsCell = React.memo(({rowData, cellData}) => {
return (
<div className="mythic-tag-cell items-center flex gap-3 mythic-tag-cell-fill min-w-0 overflow-hidden h-full w-full">
<ViewEditTags target_object={"callback_id"} target_object_id={rowData.id} target_object_display_id={rowData.display_id} />
<div className="mythic-tag-list items-center flex flex-fill flex-nowrap gap-2 mythic-callback-tag-list min-w-0 overflow-hidden">
{cellData?.length > 0 ?
<TagsDisplay tags={cellData} expand={false} /> :
<span className="mythic-callback-tagsEmpty text-xs font-650 truncate text-disabled whitespace-nowrap">No tags</span>
}
</div>
</div>
)
}, areEqual)
@@ -1,20 +1,31 @@
import React, { useEffect } from 'react';
import AppBar from '@mui/material/AppBar';
import Tabs from '@mui/material/Tabs';
import {
DragDropContext,
Droppable,
} from "@hello-pangea/dnd";
import { CallbacksTabsTaskingLabel, CallbacksTabsTaskingPanel } from './CallbacksTabsTasking';
import { CallbacksTabsFileBrowserLabel, CallbacksTabsFileBrowserPanel } from './CallbacksTabsFileBrowser';
import { CallbacksTabsProcessBrowserLabel, CallbacksTabsProcessBrowserPanel } from './CallbacksTabsProcessBrowser';
import { CallbacksTabsTaskingSplitLabel, CallbacksTabsTaskingSplitPanel} from "./CallbacksTabsTaskingSplit";
import {CallbacksTabsTaskingConsoleLabel, CallbacksTabsTaskingConsolePanel} from "./CallbacksTabsTaskingConsole";
import {
CallbacksTabsCustomFileBasedBrowserLabel,
CallbacksTabsCustomFileBasedBrowserPanel
} from "./CallbacksTabsCustomFileBasedBrowser";
import {CallbackBrowserTreeProvider} from "./CallbackBrowserTreeStore";
export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, onEditTabDescription, contextMenuOptions, me}) {
export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clickedTabId, setClickedTabId, onEditTabDescription, contextMenuOptions, me, newDataForTab, setNewDataForTab}) {
const mountedRef = React.useRef(true);
const [value, setValue] = React.useState(0);
const handleChange = (event, newValue) => {
setValue(newValue);
//console.log(newValue);
//console.log("clickedTab", newValue, openTabs[newValue]);
if(openTabs[newValue] === undefined){return}
localStorage.setItem('clickedTab', openTabs[newValue].tabID);
setClickedTabId(openTabs[newValue].tabID);
};
React.useEffect( () => {
return() => {
@@ -24,107 +35,168 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, o
}, [])
const onCloseTabLocal = ({ tabID, index }) => {
if (index > 0) {
setClickedTabId(openTabs[index-1].tabID);
setValue(index - 1);
} else {
//console.log("closing index", index)
} else if(openTabs.length > 0) {
//console.log("closing index length > 0", index)
setClickedTabId(openTabs[0].tabID);
setValue(0);
}
onCloseTab({ tabID, index });
};
useEffect(() => {
//console.log(clickedTabId);
//console.log(clickedTabId, openTabs);
for (let i = 0; i < openTabs.length; i++) {
//console.log("openTabs[i]", i, openTabs[i]);
if (openTabs[i].tabID === clickedTabId) {
//console.log("seting value", i);
//console.log("setting value", i);
setValue(i);
return;
}
}
}, [clickedTabId, openTabs]);
const [collapseTaskRequest, setCollapseTaskRequest] = React.useState({})
const modifiedInteractContextMenuOptions = [...contextMenuOptions,
{
name: 'Collapse All Tasks',
click: ({event, index}) => {
setCollapseTaskRequest((prevState) => {
if(prevState[index] !== undefined){
prevState[index] += 1;
return {...prevState};
} else {
return { [index]: 0};
}
});
}
},]
return (
<div style={{width: "100%", display: 'flex', flexDirection: 'column', flexGrow: 1, height: "100%" }}>
<AppBar color='default' position='static' className={"no-box-shadow"}>
<Tabs
value={value}
onChange={handleChange}
indicatorColor='primary'
textColor='primary'
variant='scrollable'
scrollButtons='auto'
style={{ maxWidth: '100%', width: '100%' }}
aria-label='scrollable auto tabs example'>
{openTabs.map((tab, index) => {
switch (tab.tabType) {
case 'interact':
return (
<CallbacksTabsTaskingLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
);
case 'interactSplit':
return (
<CallbacksTabsTaskingSplitLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
)
case 'interactConsole':
return (
<CallbacksTabsTaskingConsoleLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
)
case 'fileBrowser':
return (
<CallbacksTabsFileBrowserLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
);
case 'processBrowser':
return (
<CallbacksTabsProcessBrowserLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
);
default:
return null;
}
})}
</Tabs>
<CallbackBrowserTreeProvider openTabs={openTabs}>
<div style={{width: "100%", maxWidth: "100%", display: 'flex', flexDirection: 'column',
flexGrow: 1, height: "100%", }}>
<AppBar color='default' position={"static"} className="no-box-shadow mythic-table-toolbar flex flex-none flex-wrap gap-4 w-full rounded bg-surface-muted border-subtle"
style={{marginBottom: "0.2rem"}}>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="callback-tabs-list" direction={"horizontal"}>
{(provided) => (
<>
<Tabs
ref={provided.innerRef} {...provided.droppableProps}
value={value}
onChange={handleChange}
indicatorColor='primary'
textColor='primary'
variant='fullWidth'
scrollButtons={true}
TabIndicatorProps={{style: {display: "none"}}}
aria-label='scrollable tabs'>
{openTabs.map((tab, index) => {
switch (tab.tabType) {
case 'interact':
return (
<CallbacksTabsTaskingLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
newDataForTab={newDataForTab}
selectedIndex={value}
onDragTab={onDragTab}
contextMenuOptions={modifiedInteractContextMenuOptions}
/>
);
case 'interactSplit':
return (
<CallbacksTabsTaskingSplitLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
newDataForTab={newDataForTab}
selectedIndex={value}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
)
case 'interactConsole':
return (
<CallbacksTabsTaskingConsoleLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
newDataForTab={newDataForTab}
selectedIndex={value}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
)
case 'customFileBasedBrowser':
return (
<CallbacksTabsCustomFileBasedBrowserLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType + tab?.customBrowser?.name}
tabInfo={tab}
index={index}
me={me}
newDataForTab={newDataForTab}
selectedIndex={value}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
);
case 'processBrowser':
return (
<CallbacksTabsProcessBrowserLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
newDataForTab={newDataForTab}
selectedIndex={value}
onDragTab={onDragTab}
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;
}
})}
{provided.placeholder}
</Tabs>
</>
)}
</Droppable>
</DragDropContext>
</AppBar>
{openTabs.map((tab, index) => {
@@ -141,9 +213,11 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, o
key={'tabpanel' + tab.tabID + tab.tabType}
onCloseTab={onCloseTabLocal}
tabInfo={tab}
setNewDataForTab={setNewDataForTab}
value={value}
index={index}
me={me}
collapseTaskRequest={collapseTaskRequest}
parentMountedRef={mountedRef}
/>
);
@@ -159,6 +233,7 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, o
key={'tabpanel' + tab.tabID + tab.tabType}
onCloseTab={onCloseTabLocal}
tabInfo={tab}
setNewDataForTab={setNewDataForTab}
value={value}
index={index}
me={me}
@@ -177,12 +252,32 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, o
key={'tabpanel' + tab.tabID + tab.tabType}
onCloseTab={onCloseTabLocal}
tabInfo={tab}
setNewDataForTab={setNewDataForTab}
value={value}
index={index}
me={me}
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
@@ -195,10 +290,12 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, o
onCloseTab={onCloseTabLocal}
key={'tabpanel' + tab.tabID + tab.tabType}
tabInfo={tab}
setNewDataForTab={setNewDataForTab}
value={value}
index={index}
me={me}
parentMountedRef={mountedRef}
baseUIFeature={"file_browser"}
/>
);
case 'processBrowser':
@@ -213,6 +310,7 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, o
onCloseTab={onCloseTabLocal}
key={'tabpanel' + tab.tabID + tab.tabType}
tabInfo={tab}
setNewDataForTab={setNewDataForTab}
value={value}
index={index}
me={me}
@@ -224,5 +322,6 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, o
}
})}
</div>
</CallbackBrowserTreeProvider>
);
}
@@ -1,23 +1,36 @@
import {MythicActionButton} from "../../MythicComponents/MythicActionButton";
import { MythicTabPanel, MythicTabLabel } from '../../MythicComponents/MythicTabPanel';
import React, { useEffect, useCallback } from 'react';
import { gql, useLazyQuery, useQuery, useSubscription } from '@apollo/client';
import { gql, useLazyQuery, useSubscription } from '@apollo/client';
import { snackActions } from '../../utilities/Snackbar';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
import MythicTextField from '../../MythicComponents/MythicTextField';
import Grid from '@mui/material/Grid';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import RefreshIcon from '@mui/icons-material/Refresh';
import IconButton from '@mui/material/IconButton';
import { CallbacksTabsFileBrowserTree } from './CallbacksTabsFileBrowserTree';
import { CallbacksTabsFileBrowserTable } from './CallbacksTabsFileBrowserTable';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import VisibilityIcon from '@mui/icons-material/Visibility';
import { MythicModifyStringDialog } from '../../MythicComponents/MythicDialog';
import { Backdrop } from '@mui/material';
import {Backdrop} from '@mui/material';
import {CircularProgress} from '@mui/material';
import {TaskFromUIButton} from './TaskFromUIButton';
import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip';
import Split from 'react-split';
import {subscriptionCallbackTokens} from "./CallbacksTabsTaskingInput";
import {CallbacksTabsTaskingInputTokenSelect} from "./CallbacksTabsTaskingInputTokenSelect";
import KeyboardReturnIcon from '@mui/icons-material/KeyboardReturn';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import {b64DecodeUnicode} from "./ResponseDisplay";
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
import PlaylistRemoveIcon from '@mui/icons-material/PlaylistRemove';
import {GetMythicSetting, useSetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
import {RenderSingleTask} from "../SingleTaskView/SingleTaskView";
import {loadedCommandsQuery} from "./CallbacksTabsProcessBrowser";
import {getSkewedNow} from "../../utilities/Time";
import {getDefaultBrowserSelection, useCallbackBrowserTree} from "./CallbackBrowserTreeStore";
const fileDataFragment = gql`
fragment fileObjData on mythictree {
@@ -26,6 +39,8 @@ const fileDataFragment = gql`
task_id
filemeta {
id
agent_file_id
filename_text
}
tags {
tagtype {
@@ -33,10 +48,12 @@ const fileDataFragment = gql`
color
id
}
id
}
host
id
can_have_children
has_children
success
full_path_text
name_text
@@ -51,38 +68,72 @@ const fileDataFragment = gql`
}
}
`;
const rootFileQuery = gql`
${fileDataFragment}
query myRootFolderQuery {
mythictree(where: { parent_path_text: { _eq: "" }, tree_type: {_eq: "file"} }) {
...fileObjData
}
}
`;
const folderQuery = gql`
${fileDataFragment}
query myFolderQuery($parent_path_text: String!) {
mythictree(
query myFolderQuery($parent_path_text: String!, $parents: [String]) {
children: mythictree(
where: { parent_path_text: { _eq: $parent_path_text }, tree_type: {_eq: "file"} }
order_by: { can_have_children: asc, name: asc }
) {
...fileObjData
}
parents: mythictree(
where: { full_path_text: { _in: $parents }, tree_type: {_eq: "file"} }
order_by: { can_have_children: asc, name: asc }
) {
...fileObjData
}
self: mythictree(
where: { full_path_text: { _eq: $parent_path_text }, tree_type: {_eq: "file"} }
order_by: { can_have_children: asc, name: asc }
) {
...fileObjData
}
}
`;
const fileDataSubscription = gql`
${fileDataFragment}
subscription liveData($now: timestamp!) {
mythictree_stream(
batch_size: 1000,
const fileBrowserTaskSub = gql`
subscription fileBrowserTasks($now: timestamp!, $callback_id: Int!){
task_stream(
batch_size: 10,
cursor: {initial_value: {timestamp: $now}},
where: {tree_type: {_eq: "file"} }
) {
...fileObjData
where: {tasking_location: {_eq: "file_browser"}, callback_id: {_eq: $callback_id}}
){
id
status
completed
}
}
`;
export const getAllParentNodes = (node) => {
let linuxPathSeparatorIndex = node.full_path_text.indexOf("/");
let windowsPathSeparatorIndex = node.full_path_text.indexOf("\\");
let newNodes = [node.full_path_text];
if(linuxPathSeparatorIndex !== -1){
let pieces = node.full_path_text.split("/");
newNodes.push("/");
for(let i = 0; i < pieces.length; i++){
let newElement = pieces.slice(0, i).join("/");
if(newElement === ""){continue}
newNodes.push(newElement)
}
}
if(windowsPathSeparatorIndex !== -1){
let pieces = node.full_path_text.split("\\");
for(let i = 0; i < pieces.length; i++){
newNodes.push(pieces.slice(0, i).join("\\"))
}
}
return newNodes;
}
const mergeUniqueById = (existing = [], incoming = []) => [
...new Map([...existing, ...incoming].map((item) => [item.id, item])).values(),
];
const mergeUniqueValues = (existing = [], incoming = []) => [
...new Map([...existing, ...incoming].map((item) => [
item && typeof item === "object" ? JSON.stringify(item) : `${typeof item}:${item}`,
item,
])).values(),
];
export function CallbacksTabsFileBrowserLabel(props) {
const [description, setDescription] = React.useState('File Browser: ' + props.tabInfo.displayID);
const [openEditDescriptionDialog, setOpenEditDescriptionDialog] = React.useState(false);
@@ -114,7 +165,7 @@ export function CallbacksTabsFileBrowserLabel(props) {
]);
return (
<React.Fragment>
<MythicTabLabel label={description} onDragTab={props.onDragTab} {...props} contextMenuOptions={contextMenuOptions} />
<MythicTabLabel label={description} highlight={props.newDataForTab[props.tabInfo.tabID]} onDragTab={props.onDragTab} {...props} contextMenuOptions={contextMenuOptions} />
{openEditDescriptionDialog && (
<MythicDialog
fullWidth={true}
@@ -124,7 +175,7 @@ export function CallbacksTabsFileBrowserLabel(props) {
}}
innerDialog={
<MythicModifyStringDialog
title={"Edit Tab's Description"}
title={"Edit Tab's Description - Displays as one line"}
onClose={() => {
setOpenEditDescriptionDialog(false);
}}
@@ -137,101 +188,122 @@ export function CallbacksTabsFileBrowserLabel(props) {
</React.Fragment>
);
}
export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) => {
const [fromNow, setFromNow] = React.useState((new Date()));
export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNewDataForTab, baseUIFeature }) => {
const fromNow = React.useRef(getSkewedNow());
const active = index === value;
const markInactiveChange = useCallback(() => {
setNewDataForTab((previous) => previous[tabInfo.tabID] ? previous : {
...previous,
[tabInfo.tabID]: true,
});
}, [setNewDataForTab, tabInfo.tabID]);
const {
treeRootDataRef,
treeAdjMtx,
version: treeVersion,
hydrated: treeHydrated,
setTreeAdjMtx,
} = useCallbackBrowserTree({
treeType: "file",
mode: "file",
active,
onInactiveChange: markInactiveChange,
});
const [backdropOpen, setBackdropOpen] = React.useState(false);
const treeRootDataRef = React.useRef({}); // hold all of the actual data
const [treeAdjMtx, setTreeAdjMtx] = React.useState({}); // hold the simple adjacency matrix for parent/child relationships
const [selectedFolderData, setSelectedFolderData] = React.useState({
full_path_text: '.',
host: "",
id: "",
full_path_text: '',
host: tabInfo.host,
group: "Default",
parent_path_text: ""
});
const [showDeletedFiles, setShowDeletedFiles] = React.useState(false);
const [openTaskingButton, setOpenTaskingButton] = React.useState(false);
const taskingData = React.useRef({"parameters": "", "ui_feature": "file_browser:list"});
const mountedRef = React.useRef(true);
const tableOpenedPathIdRef = React.useRef(0);
useQuery(rootFileQuery, {
onCompleted: (data) => {
// use an adjacency matrix but only for full_path_text -> children, not both directions
// create the top level data in the treeRootDataRef
for(let i = 0; i < data.mythictree.length; i++){
let currentGroups = data.mythictree[i]?.["callback"]?.["mythictree_groups"] || ["Unknown Callbacks"];
for(let j = 0; j < currentGroups.length; j++){
if(treeRootDataRef.current[currentGroups[j]] === undefined){
treeRootDataRef.current[currentGroups[j]] = {};
}
if( treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]] === undefined) {
// new host discovered
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]] = {};
}
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] = {...data.mythictree[i]}
}
}
// create the top level data in the adjacency matrix
const newMatrix = data.mythictree.reduce( (prev, cur) => {
let currentGroups = cur?.["callback"]?.["mythictree_groups"] || ["Unknown Callbacks"];
for(let j = 0; j < currentGroups.length; j++) {
if (prev[currentGroups[j]] === undefined) {
prev[currentGroups[j]] = {};
}
if (prev[currentGroups[j]][cur["host"]] === undefined) {
// the current host isn't tracked in the adjacency matrix, so add it
prev[currentGroups[j]][cur["host"]] = {}
}
if (prev[currentGroups[j]][cur["host"]][cur["parent_path_text"]] === undefined) {
// the current parent's path isn't tracked, so add it and ourselves as children
prev[currentGroups[j]][cur["host"]][cur["parent_path_text"]] = {};
}
prev[currentGroups[j]][cur["host"]][cur["parent_path_text"]][cur["full_path_text"]] = 1;
}
return prev;
}, {...treeAdjMtx});
//console.log(treeRootDataRef.current);
setTreeAdjMtx(newMatrix);
},
fetchPolicy: 'no-cache',
const taskingTableTopTypedDataRef = React.useRef({
path: "",
token: 0,
});
useSubscription(fileDataSubscription, {
variables: {now: fromNow},
const autoTaskLsOnEmptyDirectoriesRef = React.useRef(false);
const taskingData = React.useRef({"parameters": "", "ui_feature": baseUIFeature +":list"});
const mountedRef = React.useRef(true);
const tableOpenedPathIdRef = React.useRef({
host: "",
group: "",
full_path_text: ""
});
const loadedCommandsRef = React.useRef({});
const loadingCommandsRef = React.useRef(false);
const initializedSelectionRef = React.useRef(false);
React.useEffect(() => {
if(!treeHydrated || initializedSelectionRef.current){return;}
const selection = getDefaultBrowserSelection(
treeRootDataRef.current,
treeAdjMtx,
tabInfo.callbackID,
tabInfo.host,
);
setSelectedFolderData({
full_path_text: '',
host: selection.host,
group: selection.group,
parent_path_text: "",
});
initializedSelectionRef.current = true;
}, [tabInfo.callbackID, tabInfo.host, treeAdjMtx, treeHydrated, treeRootDataRef, treeVersion]);
React.useEffect(() => {
if(!active || !initializedSelectionRef.current){return;}
setSelectedFolderData((previous) => {
const current = treeRootDataRef.current[previous.group]?.[previous.host]?.[previous.full_path_text];
if(!current){return previous;}
return {...current, group: previous.group, fromHistory: previous.fromHistory};
});
}, [active, treeRootDataRef, treeVersion]);
useSubscription(fileBrowserTaskSub, {
variables: {now: fromNow.current, callback_id: tabInfo.callbackID},
fetchPolicy: "no-cache",
ignoreResults: true,
onData: ({data}) => {
for(let i = 0; i < data.data.mythictree_stream.length; i++){
let currentGroups = data.data.mythictree_stream[i]?.["callback"]?.["mythictree_groups"] || ["Unknown Callbacks"];
for(let j = 0; j < currentGroups.length; j++) {
if (treeRootDataRef.current[currentGroups[j]] === undefined) {
treeRootDataRef.current[currentGroups[j]] = {};
}
if (treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]] === undefined) {
// new host discovered
treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]] = {};
}
treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]] = {...data.data.mythictree_stream[i]};
for(let i = 0; i < data.data.task_stream.length; i++){
if(data.data.task_stream[i].status.toLowerCase().includes("error") && data.data.task_stream[i].completed){
snackActions.error(<RenderSingleTask task_id={data.data.task_stream[i].id} />,
{toastId: data.data.task_stream[i].id, autoClose: false, closeOnClick: false});
}
}
const newMatrix = data.data.mythictree_stream.reduce( (prev, cur) => {
let currentGroups = cur?.["callback"]?.["mythictree_groups"] || ["Unknown Callbacks"];
for(let j = 0; j < currentGroups.length; j++) {
if (prev[currentGroups[j]] === undefined) {
prev[currentGroups[j]] = {};
}
if (prev[currentGroups[j]][cur["host"]] === undefined) {
// the current host isn't tracked in the adjacency matrix, so add it
prev[currentGroups[j]][cur["host"]] = {}
}
if (prev[currentGroups[j]][cur["host"]][cur["parent_path_text"]] === undefined) {
// the current parent's path isn't tracked, so add it and ourselves as children
prev[currentGroups[j]][cur["host"]][cur["parent_path_text"]] = {};
}
prev[currentGroups[j]][cur["host"]][cur["parent_path_text"]][cur["full_path_text"]] = 1;
}
return prev;
}, {...treeAdjMtx});
setTreeAdjMtx(newMatrix);
}
})
const [getLoadedCommandsQuery] = useLazyQuery(loadedCommandsQuery, {
fetchPolicy: 'no-cache',
onCompleted: (data) => {
if(data.loadedcommands.length > 0){
loadedCommandsRef.current[data.loadedcommands[0].callback_id] = [...data.loadedcommands];
}
loadingCommandsRef.current = false;
},
onError: (data) => {
console.log(data);
loadingCommandsRef.current = false;
}
});
async function getLoadedCommandForUIFeature (callback_id, uifeature){
while(loadingCommandsRef.current){
await new Promise(resolve => setTimeout(resolve, 50));
}
if(loadedCommandsRef.current[callback_id] === undefined){
loadingCommandsRef.current = true;
getLoadedCommandsQuery({variables: {callback_id: callback_id}});
while(loadingCommandsRef.current){
await new Promise(resolve => setTimeout(resolve, 50));
}
}
if(loadedCommandsRef.current[callback_id] === undefined){
return undefined;
}
for(let i = 0; i < loadedCommandsRef.current[callback_id].length; i++){
if(loadedCommandsRef.current[callback_id][i].command.supported_ui_features.includes(uifeature)){
return loadedCommandsRef.current[callback_id][i];
}
}
return undefined;
}
const [getFolderData] = useLazyQuery(folderQuery, {
onError: (data) => {
console.error(data);
@@ -240,23 +312,74 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
notifyOnNetworkStatusChange: true,
onCompleted: (data) => {
//console.log("getFolderData", data)
let mythictree = [...data.parents, ...data.children];
snackActions.dismiss();
// add in all of the raw data
for(let i = 0; i < data.mythictree.length; i++){
let currentGroups = data.mythictree[i]?.["callback"]?.["mythictree_groups"] || ["Unknown Callbacks"];
for(let i = 0; i < mythictree.length; i++){
let currentGroups = mythictree[i]?.["callback"]?.["mythictree_groups"] || ["Unknown Callbacks"];
for(let j = 0; j < currentGroups.length; j++){
if(treeRootDataRef.current[currentGroups[j]] === undefined){
treeRootDataRef.current[currentGroups[j]] = {};
}
if( treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]] === undefined) {
if( treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]] === undefined) {
// new host discovered
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]] = {};
treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]] = {};
}
if(treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]][mythictree[i]["full_path_text"]] === undefined){
// first time we're seeing this file data, just add it
treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]][mythictree[i]["full_path_text"]] = {...mythictree[i]};
treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]][mythictree[i]["full_path_text"]].filemeta = treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]][mythictree[i]["full_path_text"]].filemeta.map(f => {
return {...f, filename_text: b64DecodeUnicode(f.filename_text)}
})
if(selectedFolderData.group === currentGroups[j] && selectedFolderData.host === mythictree[i]["host"] &&
selectedFolderData.full_path_text === mythictree[i]["full_path_text"]){
setSelectedFolderData({...treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]][mythictree[i]["full_path_text"]],
group: currentGroups[j], fromHistory: selectedFolderData.fromHistory});
}
} 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){
existingData.deleted = mythictree[i].deleted;
}
if( (existingData.success === null || !existingData.success) && mythictree[i].success !== null){
existingData.success = mythictree[i].success;
existingData.task_id = mythictree[i].task_id;
}
existingData.comment += mythictree[i].comment;
existingData.tags = mergeUniqueById(existingData.tags, mythictree[i].tags);
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)){
existingData.metadata.permissions = mergeUniqueValues(existingData.metadata.permissions, mythictree[i].metadata.permissions);
} else if(Array.isArray(existingData.metadata.permissions) !== Array.isArray(mythictree[i].metadata.permissions)) {
// had array/dict and got mismatched array/dict, so just take newer one
existingData.metadata.permissions = mythictree[i].metadata.permissions;
} else {
existingData.metadata.permissions = {...existingData.metadata.permissions, ...mythictree[i].metadata.permissions};
}
}
let newfileData = mythictree[i].filemeta.map(f => {
return {...f, filename_text: b64DecodeUnicode(f.filename_text)};
})
existingData.filemeta = mergeUniqueById(existingData.filemeta, newfileData)
existingData.filemeta.sort((a,b) => a.id > b.id ? 1 : -1);
treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]][mythictree[i]["full_path_text"]] = {...existingData};
if(selectedFolderData.group === currentGroups[j] && selectedFolderData.host === mythictree[i]["host"] &&
selectedFolderData.full_path_text === mythictree[i]["full_path_text"]){
setSelectedFolderData({...existingData,
group: currentGroups[j],
fromHistory: selectedFolderData.fromHistory});
}
}
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] = {...data.mythictree[i]}
}
}
// create the top level data in the adjacency matrix
const newMatrix = data.mythictree.reduce( (prev, cur) => {
const newMatrix = mythictree.reduce( (prev, cur) => {
let currentGroups = cur?.["callback"]?.["mythictree_groups"] || ["Unknown Callbacks"];
for(let j = 0; j < currentGroups.length; j++) {
if (prev[currentGroups[j]] === undefined) {
@@ -278,37 +401,124 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
setTreeAdjMtx(newMatrix);
//console.log("just set treeAdjMtx, about to close backdrop")
setBackdropOpen(false);
if(data.self.length > 0){
//setSelectedFolderData({...selectedFolderData, task_id: data.self[0].task_id, success: data.self[0].success});
// this path exists, let's see if we have data for it and if the user wants us to auto-issue an ls for the path
let newAllData = Object.keys(newMatrix[selectedFolderData.group]?.[selectedFolderData.host]?.[data.self[0].full_path_text] || {});
if(autoTaskLsOnEmptyDirectoriesRef.current){
if(newAllData.length === 0 && data.self[0].full_path_text !== "" && data.self[0].success === null){
onListFilesButtonFromTableWithNoEntries()
}
}
} else {
// we couldn't find the path specified, so we must not have data for it, so check if the user wants us to auto issue an ls
if(autoTaskLsOnEmptyDirectoriesRef.current){
// don't want to auto ls when we get to the root/host object in the view
if(selectedFolderData.full_path_text !== ""){
onListFilesButtonFromTableWithNoEntries()
}
}
}
},
});
const onSetTableData = useCallback((nodeData) => {
//console.log("setTableData", nodeData);
setSelectedFolderData(nodeData);
}, []);
const fetchFolderData = useCallback((nodeData) => {
const fetchFolderData = useCallback((nodeData, fromHistory) => {
let parentNodes = getAllParentNodes(nodeData);
getFolderData({
variables: { parent_path_text: nodeData.full_path_text},
variables: { parent_path_text: nodeData.full_path_text, parents: parentNodes},
});
setBackdropOpen(true);
tableOpenedPathIdRef.current = nodeData.id;
setSelectedFolderData(nodeData);
setSelectedFolderData({...nodeData, fromHistory: fromHistory});
}, []);
const onListFilesButton = ({ fullPath, callback_id, callback_display_id }) => {
const fetchParentFolderData = (nodeData) => {
let parentNodes = getAllParentNodes(nodeData);
getFolderData({
variables: { parent_path_text: nodeData.parent_path_text, parents: parentNodes},
});
setBackdropOpen(true);
let parentData = treeRootDataRef.current[nodeData.group][nodeData.host][nodeData.parent_path_text];
parentData.group = nodeData.group;
tableOpenedPathIdRef.current = {
group: parentData.group,
host: parentData.host,
full_path_text: parentData.full_path_text
};
setSelectedFolderData({...parentData, fromHistory: false});
};
const localSelectedToken = React.useRef("");
const onChangeSelectedToken = (token) => {
localSelectedToken.current = token;
}
const onListFilesButton = ({ fullPath, callback_id, callback_display_id, token }) => {
let path = fullPath;
if(path === ""){
path = ".";
}
taskingData.current = ({
"parameters": {path: fullPath, full_path: fullPath, host: selectedFolderData.host, file: ""},
"ui_feature": "file_browser:list", callback_id, callback_display_id});
"token": token,
"parameters": {path: path, full_path: path, host: selectedFolderData.host, file: ""},
"ui_feature": baseUIFeature + ":list", callback_id, callback_display_id});
setOpenTaskingButton(true);
};
const onUploadFileButton = ({ fullPath, callback_id, callback_display_id }) => {
const onListFilesButtonFromTableWithNoEntries = () => {
taskingData.current = ({
"token": taskingTableTopTypedDataRef.current.token === "Default Token" ? 0 : taskingTableTopTypedDataRef.current.token,
"parameters": {
path: taskingTableTopTypedDataRef.current.path,
full_path: taskingTableTopTypedDataRef.current.path,
host: selectedFolderData.host,
file: ""
},
"ui_feature": baseUIFeature +":list",
callback_id: tabInfo.callbackID,
callback_display_id: tabInfo.displayID});
setOpenTaskingButton(true);
}
const onUploadFileButton = ({ fullPath, callback_id, callback_display_id, token }) => {
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);
};
const multipleTasks = React.useRef([]);
const onTaskRowActions = useCallback( (tasks) => {
let updatedTasks = [];
for(let i = 0; i < tasks.length; i++){
updatedTasks.push({
"token": localSelectedToken.current,
"parameters": {
host: selectedFolderData.host,
path: tasks[i].path,
full_path: tasks[i].full_path,
file: tasks[i].filename,
},
"ui_feature": tasks[i].uifeature,
openDialog: tasks[i].openDialog,
getConfirmation: tasks[i].getConfirmation,
callback_id: tasks[i].callback_id,
callback_display_id: tasks[i].callback_display_id});
}
multipleTasks.current = updatedTasks;
taskingData.current = multipleTasks.current.pop()
setOpenTaskingButton(true);
}, [selectedFolderData, localSelectedToken.current]);
React.useEffect( () => {
if(!openTaskingButton){
if(multipleTasks.current.length > 0){
taskingData.current = multipleTasks.current.pop()
setOpenTaskingButton(true);
}
}
}, [openTaskingButton]);
const onTaskRowAction = useCallback(({ path, full_path, filename, uifeature, openDialog, getConfirmation, callback_id, callback_display_id }) => {
taskingData.current = ({
"token": localSelectedToken.current,
"parameters": {
host: selectedFolderData.host,
path: path,
@@ -317,7 +527,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
}, "ui_feature": uifeature,
openDialog, getConfirmation, callback_id, callback_display_id});
setOpenTaskingButton(true);
}, [selectedFolderData]);
}, [selectedFolderData, localSelectedToken.current]);
const toggleShowDeletedFiles = (showStatus) => {
setShowDeletedFiles(showStatus);
};
@@ -329,14 +539,28 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
}, [])
const taskListing = (nodeData, callback_id, callback_display_id) => {
taskingData.current = ({
"parameters": {path: nodeData.full_path_text, full_path: nodeData.full_path_text, host: selectedFolderData.host, file: ""},
"ui_feature": "file_browser:list", callback_id, callback_display_id});
"token": localSelectedToken.current,
"parameters": {path: nodeData.full_path_text, full_path: nodeData.full_path_text, host: nodeData.host, file: ""},
"ui_feature": baseUIFeature + ":list", callback_id, callback_display_id});
setOpenTaskingButton(true);
}
const openDirectoryPath = ({group, host, path}) => {
let parentNodes = getAllParentNodes({full_path_text: path});
getFolderData({
variables: {parent_path_text: path, parents: parentNodes}
})
setBackdropOpen(true);
setSelectedFolderData({host, group, full_path_text: path, id: 0, success: null});
tableOpenedPathIdRef.current = {
group: group,
host: host,
full_path_text: path
};
}
return (
<MythicTabPanel index={index} value={value}>
<Split direction="horizontal" style={{width: "100%", height: "100%", display: "flex"}} sizes={[30, 70]} >
<div className="bg-gray-base" style={{display: "inline-flex"}}>
<Split direction="horizontal" className="mythic-eventing-split flex h-full min-h-0 min-w-0 overflow-hidden w-full" sizes={[30, 70]} >
<div className="mythic-eventing-sidebar flex flex-column min-h-0 min-w-0 overflow-hidden rounded bg-surface-raised border-subtle">
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={true}>
<CircularProgress color="inherit" />
</Backdrop>
@@ -345,70 +569,138 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
showDeletedFiles={showDeletedFiles}
treeRootData={treeRootDataRef.current}
treeAdjMatrix={treeAdjMtx}
selectedFolderData={selectedFolderData}
fetchFolderData={fetchFolderData}
setTableData={onSetTableData}
taskListing={taskListing}
tableOpenedPathId={tableOpenedPathIdRef.current}
getLoadedCommandForUIFeature={getLoadedCommandForUIFeature}
baseUIFeature={baseUIFeature}
/>
</div>
<div className="bg-gray-light" style={{display: "inline-flex"}}>
<div style={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
<div style={{ flexGrow: 0 }}>
<div className="mythic-eventing-content flex min-h-0 min-w-0 overflow-hidden rounded bg-surface-raised border-subtle">
<div className="mythic-process-browser-table-shell flex flex-column h-full min-h-0 min-w-0 overflow-hidden w-full relative">
<div>
<FileBrowserTableTop
active={active}
tabInfo={tabInfo}
taskingTableTopTypedDataRef={taskingTableTopTypedDataRef}
autoTaskLsOnEmptyDirectoriesRef={autoTaskLsOnEmptyDirectoriesRef}
onChangeSelectedToken={onChangeSelectedToken}
selectedFolderData={selectedFolderData}
onListFilesButton={onListFilesButton}
onUploadFileButton={onUploadFileButton}
toggleShowDeletedFiles={toggleShowDeletedFiles}
fetchParentFolderData={fetchParentFolderData}
fetchFolderData={fetchFolderData}
openDirectoryPath={openDirectoryPath}
baseUIFeature={baseUIFeature}
/>
</div>
<div style={{ flexGrow: 1 }}>
<div className="mythic-process-browser-grid-shell flex-fill min-h-0 min-w-0 overflow-hidden">
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={true}>
<CircularProgress color="inherit" />
</Backdrop>
<CallbacksTabsFileBrowserTable
active={active}
dataVersion={treeVersion}
tabInfo={tabInfo}
showDeletedFiles={showDeletedFiles}
onRowDoubleClick={fetchFolderData}
treeRootData={treeRootDataRef.current}
treeAdjMatrix={treeAdjMtx}
onListFilesButtonFromTableWithNoEntries={onListFilesButtonFromTableWithNoEntries}
selectedFolderData={selectedFolderData}
autoTaskLsOnEmptyDirectories={autoTaskLsOnEmptyDirectoriesRef.current}
onTaskRowAction={onTaskRowAction}
onTaskRowActions={onTaskRowActions}
getLoadedCommandForUIFeature={getLoadedCommandForUIFeature}
me={me}
baseUIFeature={baseUIFeature}
/>
</div>
</div>
</div>
</Split>
{openTaskingButton &&
<TaskFromUIButton ui_feature={taskingData.current?.ui_feature || " "}
<TaskFromUIButton
token={taskingData.current?.token || undefined}
ui_feature={taskingData.current?.ui_feature || " "}
callback_id={taskingData.current?.callback_id || tabInfo.callbackID}
parameters={taskingData.current?.parameters || ""}
tasking_location={"file_browser"}
openDialog={taskingData.current?.openDialog || false}
getConfirmation={taskingData.current?.getConfirmation || false}
selectCallback={taskingData.current?.selectCallback || false}
onTasked={() => setOpenTaskingButton(false)}/>
}
</MythicTabPanel>
);
};
const FileBrowserTableTop = ({
active,
selectedFolderData,
taskingTableTopTypedDataRef,
autoTaskLsOnEmptyDirectoriesRef,
onChangeSelectedToken,
onListFilesButton,
onUploadFileButton,
toggleShowDeletedFiles,
tabInfo
fetchParentFolderData,
fetchFolderData,
openDirectoryPath,
tabInfo,
baseUIFeature
}) => {
const [autoTaskLsOnEmptyDirectories, setAutoTaskLsOnEmptyDirectories] = React.useState(() => GetMythicSetting({
setting_name: "autoTaskLsOnEmptyDirectories", default_value: false
}));
const [updateMythicSetting] = useSetMythicSetting();
const [fullPath, setFullPath] = React.useState('');
const selectedToken = React.useRef("Default Token");
const [tokenOptions, setTokenOptions] = React.useState([]);
const [placeHolder, setPlaceHolder] = React.useState(selectedFolderData.host);
const [showDeletedFiles, setLocalShowDeletedFiles] = React.useState(false);
const [history, setHistory] = React.useState([]);
const [historyIndex, setHistoryIndex] = React.useState(0);
const onChangePath = (_, value) => {
setFullPath(value);
taskingTableTopTypedDataRef.current.path = value;
taskingTableTopTypedDataRef.current.token = selectedToken;
};
const changeSelectedToken = (token) => {
onChangeSelectedToken(token);
if(token === "Default Token"){
selectedToken.current = "Default Token";
taskingTableTopTypedDataRef.current.token = selectedToken;
return;
}
if(token.token_id !== selectedToken.current.token_id){
selectedToken.current = token;
taskingTableTopTypedDataRef.current.token = selectedToken;
}
}
useSubscription(subscriptionCallbackTokens, {
variables: {callback_id: tabInfo.callbackID}, fetchPolicy: "no-cache",
skip: !active,
ignoreResults: true,
shouldResubscribe: true,
onData: ({data}) => {
setTokenOptions(data.data.callbacktoken);
if(data.data.callbacktoken.length === 0) {
onChangeSelectedToken("Default Token");
}
}
});
useEffect(() => {
if (selectedFolderData.full_path_text !== undefined) {
if(selectedFolderData.root) {
setFullPath("");
taskingTableTopTypedDataRef.current.path = "";
} else if (selectedFolderData.full_path_text !== undefined) {
setFullPath(selectedFolderData.full_path_text);
taskingTableTopTypedDataRef.current.path = selectedFolderData.full_path_text;
}
const groups = selectedFolderData?.callback?.mythictree_groups?.join(", ") || "";
if(groups.length > 0 ){
@@ -416,59 +708,204 @@ const FileBrowserTableTop = ({
} else {
setPlaceHolder(selectedFolderData.host + groups);
}
}, [selectedFolderData]);
const onLocalListFilesButton = () => {
if (fullPath === '') {
snackActions.warning('Must provide a path to list');
useEffect( () => {
if(selectedFolderData.fromHistory){
return;
}
onListFilesButton({ fullPath });
if(selectedFolderData.id !== ""){
setHistory((currentHistory) => {
if(currentHistory[0]?.full_path_text === selectedFolderData.full_path_text){
return currentHistory;
}
return [selectedFolderData, ...currentHistory].slice(0, 20);
});
setHistoryIndex(0);
}
}, [selectedFolderData])
const moveIndexToPreviousListing = () => {
// we're getting closer to the end of the historyRef.current, the oldest listing
if(historyIndex >= history.length -1){
return
}
setHistoryIndex(historyIndex + 1);
fetchFolderData(history[historyIndex + 1], true);
}
const moveIndexToNextListing = () => {
// we're getting close to index 0, the newest listing
//console.log(historyIndex, history);
if(historyIndex <= 0){
return
}
setHistoryIndex(historyIndex - 1);
fetchFolderData(history[historyIndex - 1], true);
}
const onLocalListFilesButton = () => {
let tempPath = fullPath;
if (tempPath === "") {
tempPath = ".";
}
if(tempPath.length > 1){
if(tempPath[tempPath.length-1] === "/" || tempPath[tempPath.length-1] === "\\"){
let newFullPath = tempPath.slice(0, tempPath.length-1);
setFullPath(newFullPath);
onListFilesButton({ fullPath: newFullPath, token: selectedToken.current });
return
}
}
onListFilesButton({ fullPath: tempPath, token: selectedToken.current });
};
const onLocalUploadFileButton = () => {
onUploadFileButton({ fullPath });
onUploadFileButton({ fullPath, token: selectedToken.current });
};
const onLocalToggleShowDeletedFiles = () => {
setLocalShowDeletedFiles(!showDeletedFiles);
toggleShowDeletedFiles(!showDeletedFiles);
};
const onLocalMoveUpDirectoryButton = () => {
if(selectedFolderData.parent_path_text !== ""){
fetchParentFolderData(selectedFolderData)
}
};
const onToggleAutoTaskLsOnEmptyDirectories = () => {
const nextAutoTaskSetting = !autoTaskLsOnEmptyDirectories;
setAutoTaskLsOnEmptyDirectories(nextAutoTaskSetting);
autoTaskLsOnEmptyDirectoriesRef.current = nextAutoTaskSetting;
updateMythicSetting({
setting_name: "autoTaskLsOnEmptyDirectories",
value: nextAutoTaskSetting,
broadcast: false,
});
if(autoTaskLsOnEmptyDirectories){
snackActions.info("No longer auto issuing listings for empty paths");
} else {
snackActions.success("Now starting to auto issue listings for empty paths");
}
}
const goToDirectory = () => {
if(fullPath === ""){return}
if(fullPath.length > 1){
if(fullPath[fullPath.length-1] === "/" || fullPath[fullPath.length-1] === "\\"){
let newFullPath = fullPath.slice(0, fullPath.length-1);
setFullPath(newFullPath);
openDirectoryPath({
group: selectedFolderData.group,
host: selectedFolderData.host,
path: newFullPath,
})
return
}
}
openDirectoryPath({
group:selectedFolderData.group,
host: selectedFolderData.host,
path: fullPath,
})
}
useEffect( () => {
autoTaskLsOnEmptyDirectoriesRef.current = autoTaskLsOnEmptyDirectories;
}, [autoTaskLsOnEmptyDirectories]);
return (
<Grid container spacing={0} style={{ paddingTop: '10px' }}>
<Grid item xs={12}>
<Grid container spacing={0} className="mythic-file-browser-tableTop">
<Grid size={12}>
<MythicTextField
placeholder={selectedFolderData.host}
value={fullPath}
onEnter={onLocalListFilesButton}
onEnter={goToDirectory}
onChange={onChangePath}
name={placeHolder}
marginTop="0"
marginBottom="0"
InputProps={{
className: "mythic-file-browser-pathInput",
endAdornment: (
<React.Fragment>
<div className="mythic-file-browser-toolbarGroup items-center inline-flex mythic-file-browser-toolbarGroupEnd">
<MythicStyledTooltip title={`Task current callback (${tabInfo["displayID"]}) to list contents`}>
<IconButton style={{ padding: '3px' }} onClick={onLocalListFilesButton} size="large">
<RefreshIcon color='info' />
</IconButton>
<MythicActionButton iconOnly
appearance="plain" colorMode="hover" shape="square" tone="info"
onClick={onLocalListFilesButton}
size="small">
<RefreshIcon fontSize="small" />
</MythicActionButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={`Upload file to folder via current callback (${tabInfo["displayID"]})`}>
<IconButton style={{ padding: '3px' }} onClick={onLocalUploadFileButton} size="large">
<CloudUploadIcon color="info" />
</IconButton>
<MythicActionButton iconOnly
appearance="plain" colorMode="hover" shape="square" tone="info"
onClick={onLocalUploadFileButton}
size="small">
<CloudUploadIcon fontSize="small" />
</MythicActionButton>
</MythicStyledTooltip>
{autoTaskLsOnEmptyDirectories ? (
<MythicStyledTooltip title={"Currently tasking listing on empty directories, click to toggle off"} >
<MythicActionButton iconOnly
appearance="plain" active colorMode="hover" shape="square" tone="success"
onClick={onToggleAutoTaskLsOnEmptyDirectories}
size="small">
<PlaylistAddIcon fontSize="small" />
</MythicActionButton>
</MythicStyledTooltip>
) : (
<MythicStyledTooltip title={"Currently not tasking listing on empty directories, click to toggle on"} >
<MythicActionButton iconOnly
appearance="plain" colorMode="hover" shape="square" tone="warning"
onClick={onToggleAutoTaskLsOnEmptyDirectories}
size="small">
<PlaylistRemoveIcon fontSize="small" />
</MythicActionButton>
</MythicStyledTooltip>
)}
<MythicStyledTooltip title={showDeletedFiles ? 'Hide Deleted Files' : 'Show Deleted Files'}>
<IconButton
style={{ padding: '3px' }}
<MythicActionButton iconOnly
appearance="plain" active={showDeletedFiles} colorMode="hover" shape="square" tone="warning"
onClick={onLocalToggleShowDeletedFiles}
size="large">
size="small">
{showDeletedFiles ? (
<VisibilityIcon color="success" />
<VisibilityIcon fontSize="small" />
) : (
<VisibilityOffIcon color="error" />
<VisibilityOffIcon fontSize="small" />
)}
</IconButton>
</MythicActionButton>
</MythicStyledTooltip>
</React.Fragment>
</div>
),
style: { padding: 0 },
startAdornment: (
<div className="mythic-file-browser-toolbarGroup items-center inline-flex mythic-file-browser-toolbarGroupStart">
{tokenOptions.length > 0 &&
<div className="mythic-file-browser-tokenSelect overflow-hidden">
<CallbacksTabsTaskingInputTokenSelect width={"100%"}
options={tokenOptions} changeSelectedToken={changeSelectedToken}/>
</div>
}
<MythicStyledTooltip title={`Move back to previous listing`}>
<MythicActionButton iconOnly
appearance="plain" colorMode="hover" shape="square" tone="info"
disabled={historyIndex >= history.length -1 }
onClick={moveIndexToPreviousListing}
size="small">
<ArrowBackIcon fontSize="small" />
</MythicActionButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={`Move to next listing`}>
<MythicActionButton iconOnly
appearance="plain" colorMode="hover" shape="square" tone="info"
disabled={historyIndex <= 0}
onClick={moveIndexToNextListing}
size="small">
<ArrowForwardIcon fontSize="small" />
</MythicActionButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Move up a directory"} >
<MythicActionButton iconOnly
appearance="plain" colorMode="hover" shape="square" tone="info"
onClick={onLocalMoveUpDirectoryButton}
disabled={!selectedFolderData?.parent_path_text || selectedFolderData?.parent_path_text?.length === 0 || selectedFolderData.root || fullPath === ""}
>
<KeyboardReturnIcon fontSize="small" style={{rotate: "90deg"}} />
</MythicActionButton>
</MythicStyledTooltip>
</div>),
}}
/>
</Grid>
File diff suppressed because it is too large. Load diff
@@ -1,54 +1,176 @@
import React from 'react';
import FileBrowserVirtualTree from '../../MythicComponents/MythicFileBrowserVirtualTree';
import {FileBrowserVirtualTree} from '../../MythicComponents/MythicFileBrowserVirtualTree';
import {MythicDialog} from "../../MythicComponents/MythicDialog";
import {ViewCallbackMythicTreeGroupsDialog} from "./ViewCallbackMythicTreeGroupsDialog";
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 CallbacksTabsFileBrowserTree = ({ treeRootData, treeAdjMatrix, fetchFolderData, setTableData, taskListing, tableOpenedPathId, showDeletedFiles, tabInfo}) => {
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, baseUIFeature}) => {
const [openNodes, setOpenNodes] = React.useState({});
const groupName = React.useRef("");
const [openViewGroupsDialog, setOpenViewGroupDialog] = React.useState(false);
const lastOpenedNodeRef = React.useRef({group: "", host: "", full_path_text: ""});
const toggleNodeExpanded = (nodeId, nodeData) => {
//console.log("toggleNodeExpanded", nodeId, nodeData);
let lastOpenedNodeNewState = true;
let lastOpenedNodeID = getOpenIDFromNode(lastOpenedNodeRef.current);
if(!lastOpenedNodeRef.current?.has_children){
lastOpenedNodeNewState = false;
}
lastOpenedNodeRef.current = {...nodeData};
setTableData(nodeData);
fetchFolderData(nodeData);
setOpenNodes({
...openNodes,
[nodeId]: true
[getOpenIDFromNode(nodeData)]: true,
//[lastOpenedNodeID]: lastOpenedNodeNewState
});
};
const toggleNodeCollapsed = (nodeId, nodeData) => {
const toggleNodeCollapsed = (nodeId, nodeData) => {
setOpenNodes({
...openNodes,
[nodeId]: false
[getOpenIDFromNode(nodeData)]: false
});
};
const onSelectNode = (nodeId, nodeData) => {
const onSelectNode = React.useCallback( (nodeId, nodeData) => {
if(nodeData.root){
setTableData(nodeData);
}else if(nodeData.is_group){
groupName.current = nodeData.group;
setOpenViewGroupDialog(true);
}else {
//console.log(nodeData);
setTableData(nodeData);
//setTableData(nodeData);
toggleNodeExpanded(nodeId, nodeData);
}
};
}, [openNodes, setTableData]);
React.useEffect( () => {
let group = tableOpenedPathId.group;
let host = tableOpenedPathId.host;
let allPaths = [...getAllParentNodes(tableOpenedPathId),];
const additionalOpenNodes = allPaths.reduce( (prev, cur) => {
return {...prev, [getOpenIDFromNode({group: group, host: host, full_path_text: cur})]: true}
}, {});
setOpenNodes({
...openNodes,
[tableOpenedPathId]: true
[getOpenIDFromNode(tableOpenedPathId)]: true,
...additionalOpenNodes
});
}, [tableOpenedPathId]);
const contextMenuOptions = [
React.useEffect( () => {
let group = selectedFolderData.group;
let host = selectedFolderData.host;
let allPaths = [...getAllParentNodes(selectedFolderData)];
const additionalOpenNodes = allPaths.reduce( (prev, cur) => {
return {...prev, [getOpenIDFromNode({group: group, host: host, full_path_text: cur})]: true}
}, {});
setOpenNodes({
...openNodes,
[getOpenIDFromNode(selectedFolderData)]: true,
...additionalOpenNodes
});
}, [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: 'Task Listing',
click: ({event, node, callback_id, callback_display_id}) => {
name: 'List', type: "item", icon: <ListIcon color="warning" style={{ paddingRight: '5px'}} />,
click: ({event}) => {
event.stopPropagation();
taskListing(node, callback_id, callback_display_id);
}
},
];
const [openContextMenu, setOpenContextMenu] = React.useState(false);
const contextMenuData = React.useRef({options: []});
const onContextMenu = React.useCallback( ({event, item, itemTreeData}) => {
event.preventDefault();
event.stopPropagation();
contextMenuData.current = {item, itemTreeData,
x: event.clientX,
y:event.clientY,
dropdownAnchorRef: event.currentTarget};
if(contextMenuData.current.item?.root){
}else if(contextMenuData.current.item?.is_group){
}else {
let options = [{
name: contextMenuData.current?.item?.name_text, icon: null, disabled: true, type: "item",
click: () => {}
}];
options.push(...contextMenuOptions(tabInfo["callbackID"], tabInfo["displayID"], contextMenuData.current?.item));
if(contextMenuData.current?.item?.callback?.["display_id"] !== tabInfo["displayID"]){
options.push({
name: "Original Callback: " + contextMenuData.current?.item?.callback?.["display_id"],
icon: null, click: () => {}, type: "menu",
menuItems: [
...contextMenuOptions(contextMenuData.current?.item?.callback?.id, contextMenuData.current?.item?.callback?.display_id, contextMenuData.current?.item)
]
})
}
contextMenuData.current.options = options;
setOpenContextMenu(true);
}
}, []);
const handleMenuItemClick = (event, click) => {
event.preventDefault();
event.stopPropagation();
click({event});
//contextMenuOptions[index].click({event,
// node: {...contextMenuData.current?.item, group: contextMenuData.current?.itemTreeData?.group, host: contextMenuData.current?.itemTreeData?.host},
// callback_id, callback_display_id
//});
setOpenContextMenu(false);
};
const handleClose = (event) => {
event.preventDefault();
event.stopPropagation();
setOpenContextMenu(false);
};
//console.log(selectedFolderData)
return(
<>
<FileBrowserVirtualTree
@@ -56,16 +178,18 @@ export const CallbacksTabsFileBrowserTree = ({ treeRootData, treeAdjMatrix, fetc
treeRootData={treeRootData}
treeAdjMatrix={treeAdjMatrix}
openNodes={openNodes}
selectedFolderData={selectedFolderData}
onSelectNode={onSelectNode}
onExpandNode={toggleNodeExpanded}
onCollapseNode={toggleNodeCollapsed}
contextMenuOptions={contextMenuOptions}
onContextMenu={onContextMenu}
tabInfo={tabInfo}
baseUIFeature={baseUIFeature}
/>
{openViewGroupsDialog &&
<MythicDialog
fullWidth={true}
maxWidth={"lg"}
maxWidth={"xl"}
open={openViewGroupsDialog}
onClose={() => {setOpenViewGroupDialog(false);}}
innerDialog={
@@ -74,7 +198,51 @@ export const CallbacksTabsFileBrowserTree = ({ treeRootData, treeAdjMatrix, fetc
}
/>
}
{openContextMenu &&
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
<Dropdown
isOpen={contextMenuData.current.dropdownAnchorRef}
onOpen={setOpenContextMenu}
externallyOpen={openContextMenu}
absoluteX={contextMenuData.current.x}
absoluteY={contextMenuData.current.y}
anchorReference={"anchorPosition"}
style={{
//top: contextMenuData.current.y,
//left: contextMenuData.current.x
}}
menu={[
...contextMenuData.current?.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>
}
</>
)
};
};
export const CallbacksTabsFileBrowserTree = React.memo(CallbacksTabsFileBrowserTreePreMemo);
@@ -1,13 +1,13 @@
import React, {useEffect} from 'react';
import Button from '@mui/material/Button';
import React from 'react';
import {MythicActionButton} from '../../MythicComponents/MythicActionButton';
import DialogActions from '@mui/material/DialogActions';
import DialogTitle from '@mui/material/DialogTitle';
import {useQuery, gql } from '@apollo/client';
import {useMutation} from '@apollo/client';
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';
import {CallbacksTabsSelectTable} from "./CallbacksTabsSelectTable";
const callbacksAndFeaturesQuery = gql`
@@ -22,114 +22,18 @@ query callbacksAndFeatures{
pid
description
last_checkin
dead
payload {
payloadtype {
name
id
agent_type
}
}
mythictree_groups_string
}
}`;
const columns = [
{ field: 'display_id', headerName: 'ID', width: 80, type: 'number', },
{
field: 'host',
headerName: 'Host',
flex: 0.5,
},
{
field: 'user',
headerName: 'User',
flex: 0.5,
},
{
field: 'pid',
headerName: 'PID',
type: 'number',
width: 80,
},
{
field: 'description',
headerName: 'Description',
flex: 1,
},
{
field: 'ip',
headerName: 'IP',
flex: 1,
renderCell: (params) => <CallbacksTableIPCell rowData={params.row} cellData={params.row.ip} />,
sortable: false,
valueGetter: (params) => {
try{
return JSON.parse(params.row.ip)[0];
}catch(error){
return params.row.ip;
}
}
},
{
field: "last_checkin",
headerName: "Checkin",
width: 100,
valueGetter: (params) => new Date(params.row.last_checkin),
renderCell: (params) =>
<CallbacksTableLastCheckinCell rowData={params.row} />,
},
{
field: "payload.payloadtype.name",
headerName: "Agent",
width: 40,
valueGetter: (params) => params.row.payload.payloadtype.name,
renderCell: (params) => <CallbacksTablePayloadTypeCell rowData={params.row} />
},
{
field: "mythictree_groups_string",
headerName: "Groups",
flex: 0.5,
}
];
const CustomSelectTable = ({initialData, selectedData}) => {
const [data, setData] = React.useState([]);
const [rowSelectionModel, setRowSelectionModel] = React.useState([]);
React.useEffect( () => {
selectedData.current = data.reduce( (prev, cur) => {
if(rowSelectionModel.includes(cur.id)){return [...prev, cur]}
return [...prev];
}, []);
}, [data, rowSelectionModel]);
React.useEffect( () => {
setData(initialData.map(c => {
return {...c};
}));
}, [initialData]);
return (
<div style={{height: "calc(80vh)"}}>
<DataGrid
rows={data}
columns={columns}
initialState={{
pagination: {
paginationModel: {
},
},
sorting: {
sortModel: [{ field: 'last_checkin', sort: 'asc' }],
},
}}
autoPageSize
checkboxSelection
onRowSelectionModelChange={(newRowSelectionModel) => {
setRowSelectionModel(newRowSelectionModel);
}}
rowSelectionModel={rowSelectionModel}
density={"compact"}
/>
</div>
)
}
export function CallbacksTabsHideMultipleDialog({onClose}) {
const selectedData = React.useRef([]);
@@ -145,7 +49,7 @@ export function CallbacksTabsHideMultipleDialog({onClose}) {
onClose();
}
});
useQuery(callbacksAndFeaturesQuery,{
const {loading} = useQuery(callbacksAndFeaturesQuery,{
fetchPolicy: "no-cache",
onCompleted: (data) => {
const callbackData = data.callback.map( c => {
@@ -170,17 +74,72 @@ export function CallbacksTabsHideMultipleDialog({onClose}) {
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Hide Multiple Callbacks at Once</DialogTitle>
<CustomSelectTable initialData={initialData}
selectedData={selectedData} />
<DialogContentText style={{textAlign: "center"}}>
<b>Note: </b> Last checkin times are based on when this window opened and won't refresh.
</DialogContentText>
<CallbacksTabsSelectTable initialData={initialData}
loading={loading}
selectedData={selectedData}
sortModel={{ field: 'last_checkin', sort: 'asc' }}
tableId="hide-multiple-callbacks-table"
tableLabel="Hide multiple callbacks"
/>
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
<MythicActionButton colorMode="always" onClick={onClose} tone="primary" variant="contained">
Close
</Button>
<Button onClick={submitTasking} variant="contained" color="warning">
</MythicActionButton>
<MythicActionButton colorMode="always" onClick={submitTasking} tone="warning" variant="contained">
Hide
</Button>
</MythicActionButton>
</DialogActions>
</React.Fragment>
);
}
export function CallbacksTabsSelectMultipleDialog({onClose, onSubmit}) {
const selectedData = React.useRef([]);
const [initialData, setInitialData] = React.useState([]);
const {loading} = useQuery(callbacksAndFeaturesQuery,{
fetchPolicy: "no-cache",
onCompleted: (data) => {
const callbackData = data.callback.map( c => {
// for each callback, get a unique set of supported features
const display = `${c.display_id} - ${c.user}@${c.host} (${c.pid}) - ${c.description}`;
return {...c, display};
});
setInitialData(callbackData);
}
});
const submitTasking = () => {
if(selectedData.current.length === 0){
onClose();
return;
}
onSubmit(selectedData.current);
}
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>
<CallbacksTabsSelectTable initialData={initialData}
loading={loading}
selectedData={selectedData}
sortModel={{ field: 'display_id', sort: 'desc' }}
tableId="select-multiple-callbacks-table"
tableLabel="Select multiple callbacks"
/>
<DialogActions>
<MythicActionButton colorMode="always" onClick={onClose} tone="primary" variant="contained">
Close
</MythicActionButton>
<MythicActionButton colorMode="always" onClick={submitTasking} tone="warning" variant="contained">
Use Selection
</MythicActionButton>
</DialogActions>
</React.Fragment>
);
}
Loaded 100 of 1117 files, more files were not shown because too many files have changed in this diff. Show more