mirror of
https://github.com/its-a-feature/Mythic.git
synced 2026-11-07 17:22:06 -10:00
Compare commits
321
Commits
v3.3.0.105
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96b24b48a7 | ||
|
|
bdc722f6ce | ||
|
|
f9e71d0859 | ||
|
|
742d878928 | ||
|
|
01ea8bc896 | ||
|
|
94418bcd52 | ||
|
|
3ae9c5fe18 | ||
|
|
5e85a20d14 | ||
|
|
5f46db6ede | ||
|
|
e37e057a89 | ||
|
|
33a46d7db7 | ||
|
|
540aec0f00 | ||
|
|
29be09eef4 | ||
|
|
a5a1a972d7 | ||
|
|
82648e8241 | ||
|
|
e4da2c887f | ||
|
|
39f44f7dec | ||
|
|
0f51584493 | ||
|
|
dc50ecc0dc | ||
|
|
59211a2fbf | ||
|
|
f8d355a29c | ||
|
|
1da8fdcd2a | ||
|
|
2dcc3b5699 | ||
|
|
8deec59270 | ||
|
|
071abeaf74 | ||
|
|
6eb53da6b5 | ||
|
|
d605a2f82c | ||
|
|
31db999945 | ||
|
|
9ae193be76 | ||
|
|
fc731af819 | ||
|
|
d21dbf293b | ||
|
|
b6d98b1794 | ||
|
|
448a57b82b | ||
|
|
1d54d5ea0c | ||
|
|
4d793d5998 | ||
|
|
60455c26cb | ||
|
|
183f0605c2 | ||
|
|
37d3cc8b1b | ||
|
|
9532b96f7a | ||
|
|
e1806f0fd5 | ||
|
|
fa235b6378 | ||
|
|
b87219b1e9 | ||
|
|
b5459b56c8 | ||
|
|
ca6d7d2ce0 | ||
|
|
6148c5f0fb | ||
|
|
6e14b2a472 | ||
|
|
307df57df8 | ||
|
|
4a4d95f1ca | ||
|
|
ffc5041349 | ||
|
|
ecda245963 | ||
|
|
6ea67c69b9 | ||
|
|
64d32eb116 | ||
|
|
9f7a1ce770 | ||
|
|
4a3818988c | ||
|
|
1ec6318b62 | ||
|
|
60902a6e66 | ||
|
|
ddda1a266c | ||
|
|
8dc9f351d9 | ||
|
|
d7b4d51186 | ||
|
|
d2334b79de | ||
|
|
96df4db36e | ||
|
|
a72dd1b086 | ||
|
|
89bee21894 | ||
|
|
26a4410f0a | ||
|
|
36fb4c58bf | ||
|
|
cfdf0c537d | ||
|
|
f23e123a56 | ||
|
|
5b4c210e6f | ||
|
|
3dee8797da | ||
|
|
33f48baab4 | ||
|
|
3198f304c3 | ||
|
|
09ac733d45 | ||
|
|
3e611494cc | ||
|
|
8be3c084e5 | ||
|
|
3796640835 | ||
|
|
6e23b29f22 | ||
|
|
22e5c489b2 | ||
|
|
8cdf34c3e7 | ||
|
|
b285558fce | ||
|
|
e536f59347 | ||
|
|
4c472f7e3a | ||
|
|
cc77f2b1a3 | ||
|
|
1c18703e96 | ||
|
|
f017661b3d | ||
|
|
98dd30d984 | ||
|
|
eb3cf1053b | ||
|
|
58e2c6de5f | ||
|
|
7dc66c43aa | ||
|
|
9c6afe2d8c | ||
|
|
6c5b1d2a24 | ||
|
|
fc8239eb85 | ||
|
|
180c6e1914 | ||
|
|
59c97d791f | ||
|
|
d9e1d5e6bd | ||
|
|
0f5746b4a3 | ||
|
|
71c03251fb | ||
|
|
f742ab3d0f | ||
|
|
d3ae12a220 | ||
|
|
d9a41572ac | ||
|
|
e454531999 | ||
|
|
45cf6a9cd8 | ||
|
|
5011314ba5 | ||
|
|
af9d3c40e6 | ||
|
|
03e27abdb6 | ||
|
|
e7fde3fcde | ||
|
|
51c0eeb5df | ||
|
|
0f259d1f7d | ||
|
|
d86f0d6227 | ||
|
|
3bb93efa72 | ||
|
|
652234d7e1 | ||
|
|
44d8fd168e | ||
|
|
2e418bbb4d | ||
|
|
87249b2891 | ||
|
|
dbb15fdbb3 | ||
|
|
04ebb6aba7 | ||
|
|
8a2e42c48c | ||
|
|
db682d5020 | ||
|
|
6649279e3e | ||
|
|
6d1778bb3b | ||
|
|
0484154eae | ||
|
|
99f2b6cb2f | ||
|
|
303741ee28 | ||
|
|
37a3f66d68 | ||
|
|
8e43275c27 | ||
|
|
6dfe40b274 | ||
|
|
49e1c62502 | ||
|
|
a739837c95 | ||
|
|
063f70a55d | ||
|
|
fb6c3d8368 | ||
|
|
10f0eb9e7a | ||
|
|
afdd0ec9cc | ||
|
|
2391244baa | ||
|
|
9e0fbcfa3a | ||
|
|
54e6cc5d32 | ||
|
|
c56d92ee52 | ||
|
|
706e8d3ce7 | ||
|
|
29e57f1384 | ||
|
|
bf7de9ad22 | ||
|
|
e3cbdd35e7 | ||
|
|
2ca4ad507e | ||
|
|
43a8a8580c | ||
|
|
fd02fe2c9e | ||
|
|
3a6d9c4842 | ||
|
|
31e297a37f | ||
|
|
b40aed02b8 | ||
|
|
5e031f64b2 | ||
|
|
f6532b127d | ||
|
|
a383fcf8c5 | ||
|
|
307eb0dd72 | ||
|
|
29db86be7c | ||
|
|
04be0065f3 | ||
|
|
8601357379 | ||
|
|
bb3a4b2294 | ||
|
|
44149bdf2b | ||
|
|
1a2bf56c8c | ||
|
|
63867bc27a | ||
|
|
42a7869750 | ||
|
|
8479a2d091 | ||
|
|
c7357c6fee | ||
|
|
e6fd09b069 | ||
|
|
8cc47d518e | ||
|
|
ae2ed75801 | ||
|
|
7f225e51f1 | ||
|
|
c173f083dd | ||
|
|
3e3356e586 | ||
|
|
70a4ca26ec | ||
|
|
64fd83b999 | ||
|
|
180a81111d | ||
|
|
767881fd6c | ||
|
|
4240722b3a | ||
|
|
36ceaaacba | ||
|
|
bf22f85661 | ||
|
|
e7405bd62c | ||
|
|
2ab6e20162 | ||
|
|
274e41f1ee | ||
|
|
3748d68f2e | ||
|
|
e8efb8acea | ||
|
|
438bbf1b3d | ||
|
|
5812391061 | ||
|
|
07e6bedf71 | ||
|
|
1966848577 | ||
|
|
3953850496 | ||
|
|
aa1de9ef14 | ||
|
|
7dd545fb9d | ||
|
|
c997ab3a8b | ||
|
|
7e95ae879d | ||
|
|
5d4c173099 | ||
|
|
8095eee496 | ||
|
|
9ed4a8ee49 | ||
|
|
520e3a94ca | ||
|
|
9a44032528 | ||
|
|
df8b903757 | ||
|
|
3a814eca14 | ||
|
|
d169ee834e | ||
|
|
c7dab9a565 | ||
|
|
1e0202eaba | ||
|
|
405f58c68e | ||
|
|
ba593a8f94 | ||
|
|
ebf6425fd9 | ||
|
|
8903cf243a | ||
|
|
6333fef797 | ||
|
|
cf557df672 | ||
|
|
c2bbc061bf | ||
|
|
af5f4416a6 | ||
|
|
0692cad336 | ||
|
|
e244f9fcd1 | ||
|
|
dd8c548242 | ||
|
|
137b5cc4a1 | ||
|
|
92f3ee46b6 | ||
|
|
8979bd1f42 | ||
|
|
46abe5a032 | ||
|
|
b294c8ff53 | ||
|
|
11ab25a74e | ||
|
|
33d0e0b7a3 | ||
|
|
48c77ed20b | ||
|
|
64828ca26e | ||
|
|
96231b6f5e | ||
|
|
f1c1ceca91 | ||
|
|
2826b39c90 | ||
|
|
14e5a10a9a | ||
|
|
675827ecce | ||
|
|
599cb57682 | ||
|
|
2bd5da93ff | ||
|
|
4896194b0c | ||
|
|
fae956bec4 | ||
|
|
09e0b011f1 | ||
|
|
62c60601c3 | ||
|
|
461c6c689e | ||
|
|
b7d113753f | ||
|
|
1f274b0697 | ||
|
|
c52a6c66e9 | ||
|
|
74864c1803 | ||
|
|
364f87a6f2 | ||
|
|
a1094f6076 | ||
|
|
f898a09c83 | ||
|
|
c416292714 | ||
|
|
759ab426b6 | ||
|
|
cd3e5ed774 | ||
|
|
ffafcd86b9 | ||
|
|
7e9394e070 | ||
|
|
2a6bd6e50b | ||
|
|
c0f7b94b2f | ||
|
|
62c1151c47 | ||
|
|
8deb1d9913 | ||
|
|
084ab87593 | ||
|
|
977f796cb1 | ||
|
|
07722b7de5 | ||
|
|
b5cac5d3f1 | ||
|
|
da84bf5a47 | ||
|
|
ab7f539ed2 | ||
|
|
ae2d6e8dbd | ||
|
|
1b7fd846a2 | ||
|
|
1e34aeaacc | ||
|
|
8e9ca5b14a | ||
|
|
5338775d02 | ||
|
|
81c260d4da | ||
|
|
62f3c4c9c6 | ||
|
|
ee7441c599 | ||
|
|
3363e3d2f2 | ||
|
|
765018ab74 | ||
|
|
d36774e0b8 | ||
|
|
56bcba9b8d | ||
|
|
ae8f98cf0d | ||
|
|
09871f8ba3 | ||
|
|
02db11e522 | ||
|
|
db090b63ce | ||
|
|
4769c29ea1 | ||
|
|
a6cb5064c5 | ||
|
|
a725e36443 | ||
|
|
0ffbe0f0a5 | ||
|
|
dc146b8988 | ||
|
|
77166f400b | ||
|
|
7dd02aedb4 | ||
|
|
6fbf73ceb7 | ||
|
|
a13b8c88c4 | ||
|
|
b837684a79 | ||
|
|
de2d6080e9 | ||
|
|
775156efdc | ||
|
|
5d5e40369b | ||
|
|
ac353c3cca | ||
|
|
a4d61b5e0e | ||
|
|
2b4eb73bd5 | ||
|
|
b527bf7e0c | ||
|
|
4e8eecfe25 | ||
|
|
faf32c60f1 | ||
|
|
0fcd35a039 | ||
|
|
367f954050 | ||
|
|
49101478cb | ||
|
|
68630fe297 | ||
|
|
dbdd60c6fb | ||
|
|
c841189f00 | ||
|
|
714aec37cc | ||
|
|
121695b5f1 | ||
|
|
fd64fa2ba4 | ||
|
|
bef32bc01b | ||
|
|
f75cb4f853 | ||
|
|
f8e71c493b | ||
|
|
471d74de44 | ||
|
|
8cdcd3168b | ||
|
|
677ccdcc14 | ||
|
|
23e857b940 | ||
|
|
612eca9061 | ||
|
|
c16b4d0db3 | ||
|
|
d6f454374a | ||
|
|
4649197c17 | ||
|
|
c3d2bdc328 | ||
|
|
8f980dcf51 | ||
|
|
a21deb9d41 | ||
|
|
f6535dd91e | ||
|
|
b62ea98c10 | ||
|
|
301efc044e | ||
|
|
5d0b3d1a94 | ||
|
|
25799523d0 | ||
|
|
83ef0f2b3a | ||
|
|
dba92abbc1 | ||
|
|
ec51eb4f90 | ||
|
|
fcc8398d55 | ||
|
|
ba75470a64 | ||
|
|
caa23d3927 | ||
|
|
7e4603ddec | ||
|
|
5004a5a043 |
No files matched your search
@@ -120,7 +120,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5 # 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
|
||||
@@ -162,7 +162,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5 # 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
|
||||
@@ -204,7 +204,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5 # 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
|
||||
@@ -248,7 +248,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5 # 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
|
||||
@@ -292,7 +292,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5 # 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
|
||||
@@ -336,7 +336,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5 # 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
|
||||
@@ -382,7 +382,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
|
||||
with:
|
||||
context: mythic-react-docker
|
||||
file: mythic-react-docker/.docker/Dockerfile
|
||||
file: mythic-react-docker/Dockerfile
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_REACT_IMAGE_NAME }}:${{ env.VERSION }}
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_REACT_IMAGE_NAME }}:latest
|
||||
@@ -440,7 +440,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5 # 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
|
||||
@@ -552,7 +552,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
|
||||
|
||||
@@ -167,3 +167,6 @@ _ReSharper*/
|
||||
jupyter-docker/jupyter/MythicExamples/.ipynb_checkpoints/
|
||||
|
||||
nginx-docker/config/blockips.conf
|
||||
/MythicReactUI/build/
|
||||
/MythicReactUI/node_modules/
|
||||
/.gocache/
|
||||
+512
@@ -4,6 +4,518 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [3.4.36] - 2026-06-21
|
||||
|
||||
### Changed
|
||||
|
||||
- Addressed 3 issues from GitHub about spectators and overly scoped hasura filters
|
||||
|
||||
## [3.4.35] - 2026-04-16
|
||||
|
||||
### Changed
|
||||
|
||||
- Merged PR #546 to address missing return in GenerateAPITokenWebhook that would allow non-admin users to generate API Tokens for Bot accounts
|
||||
- Added server name to emitted log messages
|
||||
- Merged PR #545 to address missing operation_id in task's log messages for logging containers
|
||||
- Updated C2 profile code in the UI to properly handle `ui_position` from C2 Profile parameters in payload build
|
||||
|
||||
## [3.4.34] - 2026-04-13
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated way that SendAllOperationsMessage functions to reduce duplicate messages to event log
|
||||
- Reduced the number of lookups per error message to increment count by caching error lookup data
|
||||
- Updated event log messages to ensure proper UTF8 encoding (and removal of bad characters)
|
||||
- Using channels to ensure no duplicate messages are created due to go concurrency
|
||||
|
||||
## [3.4.33] - 2026-04-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue where payload/command syncing would error out and leave dangling database connections
|
||||
- Updated the postgres docker image to use customized database info for health checks
|
||||
|
||||
## [3.4.32] - 2026-04-06
|
||||
|
||||
### Changed
|
||||
|
||||
- Merged PR to add GET route for /auth_acs/:containerName/:IDPName that was POST only
|
||||
|
||||
## [3.4.31] - 2026-03-23
|
||||
|
||||
### Changed
|
||||
|
||||
- Removed code in `upload` to overwrite uploaded filename with destination filename in filemeta tracking
|
||||
- Updated `download` code to only write files to disk on new chunks for speed improvements
|
||||
- Updated the `rm` tracking to not blanket wildcard paths, but first check if it's a directory and make sure there's a trailing slash before wildcarding
|
||||
|
||||
## [3.4.30] - 2026-03-20
|
||||
|
||||
### Changed
|
||||
|
||||
- Added a `received_chunk_ids` map to the database for `filemeta` to track not just chunks, but which chunks are received
|
||||
- this helps prevent duplicate chunks getting counted multiple times and causing faulty downloads
|
||||
- Added some default values for `DisplayPath` that were causing null constraint errors on the database
|
||||
- Fixed a remote path null value with uploading eventing files
|
||||
|
||||
## [3.4.29] - 2026-03-18
|
||||
|
||||
### Changed
|
||||
|
||||
- Adjusted the MythicRPCFileSearch function to not return tracked copies of existing files
|
||||
|
||||
## [3.4.28] - 2026-03-17
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a bug where file transfers would fail if no remote path supplied
|
||||
|
||||
## [3.4.27] - 2026-03-16
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a bug where mythic would try to write to a null message after processing a bad message
|
||||
|
||||
## [3.4.26] - 2026-03-12
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a bug in "Callback Imports" that wouldn't recognize exising build parameter types
|
||||
|
||||
## [3.4.25] - 2026-03-04
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated rpfwd to do burst proxy reads similar to socks
|
||||
|
||||
## [3.4.24] - 2026-02-18
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated PushC2 to funnel traffic into one channel instead of two
|
||||
- Updated socks proxy reads to do a basic burst for read sizes to reduce number of concurrent waiting messages
|
||||
- this increases throughput for socks traffic with a bit more bursty memory usage
|
||||
- Updated key reflection in agent messages to omit cwd and impersonation_context fields
|
||||
- Updated empty BFS paths to be cached as well so non-existent paths aren't recomputed a bunch
|
||||
|
||||
## [3.4.23] - 2026-02-08
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the payload type syncing to resync for all payload types, not just new wrappers
|
||||
- this fixes a bug where payload types syncing for the first time after their wrapper synced wouldn't match them up
|
||||
|
||||
## [3.4.22] - 2026-02-02
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue where building payloads while c2 profiles were syncing would double issue tasks to c2 profile and hang
|
||||
- Fixed an issue here cached p2p paths weren't getting flushed properly
|
||||
|
||||
## [3.4.21] - 2026-01-14
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue where downloaded files weren't having their sizes reflected after completion in the file browser
|
||||
|
||||
## [3.4.20] - 2026-01-08
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated IterateAndAct to treat a crypto value of "" the same as "none"
|
||||
|
||||
## [3.4.19] - 2025-12-31
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated processing of skipped steps to indicate skip instead of most recent step that was updated
|
||||
|
||||
## [3.4.18] - 2025-12-23
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated how processing happens for the `update_deleted` with file browser data
|
||||
- data is now processed as part of two channels to ensure ordering and supporting multi-chunk file browser data
|
||||
|
||||
## [3.4.17] - 2025-12-23
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue where uploaded files were getting treated as folders in the file browsers
|
||||
- Fixed an issue where uploaded files would get the wrong timestamp in the file browser
|
||||
- Updated go modules
|
||||
|
||||
## [3.4.16] - 2025-12-23
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue where supplying a cron job in eventing as something other than string would crash due to type conversion
|
||||
|
||||
## [3.4.15] - 2025-12-22
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue with translation containers sometimes not getting associated with their payload types on first sync
|
||||
|
||||
## [3.4.14] - 2025-12-18
|
||||
|
||||
### Changed
|
||||
|
||||
- Added database triggers for `filemeta`, `credential`, and `keylog` to update timestamp when other fields change
|
||||
|
||||
## [3.4.13] - 2025-12-15
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated MythicRPCTaskCreate to require OperatorID, TaskID, or EventStepInstanceID
|
||||
- This is an update to make sure the new task is associated with the right user and not the callback creator
|
||||
- Updated the default tasking to Split Tasking instead of the collapsable accordian tasking
|
||||
|
||||
## [3.4.12] - 2025-12-11
|
||||
|
||||
### Changed
|
||||
|
||||
- Added more context to the Proxy start event log message
|
||||
- Added a new event log message when a Proxy stops
|
||||
- Updated the CustomBrowserExportFunctionMessage to also take in a callback group
|
||||
- Updated the SendMythicRPCFileCreate call to also allow creation by OperationID
|
||||
|
||||
## [3.4.11] - 2025-12-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Added `CustomBrowser` containers to allow customized file-like browser creation
|
||||
- This includes an "export" functionality that's a custom function you can implement per browser type
|
||||
- This includes a new field in `responses` for `custom_browser`
|
||||
- Added `SendMythicRPCCallbackTokenSearch`
|
||||
- Added `SendMythicRPCCustomBrowserSearch`
|
||||
- Added `verifier_regex` option to CommandParameters
|
||||
- Allows you to provide regex that must match the supplied value
|
||||
- Changed `mythictree` to move `has_children` out of metadata in support of CustomBrowsers
|
||||
- Changed `mythictree` to have new `display_path` field separate from `full_path` in support of CustomBrowsers
|
||||
- Added field to `payload` to track the version of the backing payload_type when the payload was created
|
||||
- This makes it easier to track payloads as you're doing payload type development/updates
|
||||
- Added `supported_c2` and `supported_wrapping` to Payload Types to track supported C2/Wrappers
|
||||
- This is now used to "resync" data rather than asking the container to resync back to Mythic
|
||||
- Changed `token_id` to int64 (from uint64) to match similar change in Postgres database to BigInt from just Integer
|
||||
- Updated operation event log auto resolver to use new `warining=true` vs `level='warning'` check
|
||||
- Added a 5s sleep between container's OnStart messages to help with multiple operations bombarding a container in rapid succession
|
||||
- Updated standard RPC timeouts to 10s instead of 5s
|
||||
- Updated auto re-hosting of files to have a 10s delay between files
|
||||
- Updated ContainerVersion out of bounds message to be a bit clearer
|
||||
- Updated `SendMythicRPCCallbackUpdate` to allow specifying the `dead` attribute
|
||||
- Updated `SendMythicRPCFileSearch` to allow specifying a PayloadUUID in addition to AgentFileID in `AgentFileID` field
|
||||
- Updated `SendMythicRPCResponseCreate` to go through the same process as an agent's `user_output`
|
||||
- This allows responses in this way to be intercepted and emitted to logging
|
||||
- Added some mutexes when syncing PayloadTypes to prevent possible race conditions
|
||||
- Updated file downloads to allow specifying a negative `total_chunks` until you're done and know how many chunks total there are
|
||||
- Added new `database` tab to file previews to run SQL queries on database right in the browser
|
||||
- Added the following new options for BuildParameter's HideConditionOperator
|
||||
- LessThan, GreaterThan, LessThanOrEqual, GreaterThanOrEqual, StartsWith, EndsWith, Contains, NotContains
|
||||
- [UI Updates](/MythicReactUI/CHANGELOG.MD#0383---2025-12-10)
|
||||
- [CLI Updates](/Mythic_CLI/Changelog.md#0321---2025-12-10)
|
||||
|
||||
## [3.4.10] - 2025-10-31
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue with c2 profile logos not getting reset if removed
|
||||
|
||||
## [3.4.9] - 2025-10-30
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue with eventing where payload data wasn't getting passed to new callback triggers in some situations
|
||||
|
||||
## [3.4.8] - 2025-10-29
|
||||
|
||||
### Changed
|
||||
|
||||
- Added support for a new MythicRPCCallbackEdgeRemove RPC call to allow developers to selectively remove edges
|
||||
- Added support for a new MythicRPCHandleAgentJson RPC call to allow developers to submit standard Mythic JSON messages here for processing after using a translation container instead of hitting a bunch of various other RPC calls to create their needed objects
|
||||
- Updating MythicRPCCallbackEdgeSearch to also return self edges - i.e. an egress' edge to Mythic
|
||||
- This is a change where before it would only return P2P edges in the graph
|
||||
- Updated MythicRPCTaskCreate to take in either AgentCallbackID or CallbackID
|
||||
- Added a few more spots in agent message processing to emit error information to the operation event log
|
||||
|
||||
## [3.4.7] - 2025-10-28
|
||||
|
||||
### Changed
|
||||
|
||||
- Added "ui_position" fields to BuildParameters and C2 Profile Parameters for display ordering when building payloads
|
||||
- Added "dynamic_query_function" option to BuildParameters to allow dynamic execution and choice creation during build process
|
||||
- Added "copy_of_file_id" to FileMeta to better tracking when Mythic creates copies of files
|
||||
- When uploading fileA to callback1, Mythic tracks this association so it can record things like upload location
|
||||
- If fileA (by same UUID) is uploaded again (callback1 with a different task or callback2), Mythic creates a copy of the file meta and tracks it
|
||||
- This allows Mythic to track the new upload location separate from the first upload
|
||||
- Both of these files are backed by the same file on disk, so deleting one deletes all copies
|
||||
- This is now better represented in the UI on the file search page
|
||||
|
||||
## [3.4.6] - 2025-10-16
|
||||
|
||||
### Changed
|
||||
|
||||
- Adjusted how file browser permissions are treated by default
|
||||
- they're left as-is and not force-converted to an array
|
||||
|
||||
## [3.4.5] - 2025-10-16
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a bug where creating payloads via scripting without providing values wasn't picking up dictionary based payload type c2 parameter deviations
|
||||
|
||||
## [3.4.4] - 2025-10-16
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated apitoken create mutation to also return creation_time
|
||||
|
||||
## [3.4.3] - 2025-10-13
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue with the SendMythicRPCCallbackGraphEdgeSearch returning null data for the callbacks
|
||||
|
||||
## [3.4.2] - 2025-10-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a bug with default dictionary values for scripting parameters
|
||||
- Updated the send event log scripting call to account for mythic_sync and level=warning values
|
||||
- Updated delegate tasking to not include the `token` field if it's 0
|
||||
|
||||
## [3.4.1] - 2025-10-09
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue with download tags not getting added to files
|
||||
- Updated last checkin time tracking to bulk update once/s instead of once/s per callback
|
||||
- the goal is to minimize unnecessary database writes that might spike the cpu for repeated actions
|
||||
- Updated to experimental golang json v2 for performance increases
|
||||
|
||||
## [3.4.0] - 2025-10-07
|
||||
|
||||
### Changed
|
||||
|
||||
- Added "selected_os" array option to some eventing triggers to additionally limit not just on payload type but OS as well
|
||||
- this is the "os" you select when you build a payload
|
||||
- Modified the UI to allow creating new eventing workflows based on existing ones with a single button
|
||||
- Added group_name, supported_os, and hide_conditions to build parameters for more dynamic build experiences
|
||||
- Added supports_multiple_c2_in_build, supports_multiple_c2_instances, c2_parameter_deviations to payload types
|
||||
- This allows payload types to slightly tweak c2 profile configurations they support and indicating how many c2s are allowed per build
|
||||
- c2 parameter deviations allow indicating parameters are not supported at all, that they have a different set of choices, and that the default value can be something else
|
||||
- Added semver option for payload types to more accurately convey their current version outside of their description
|
||||
- Updated the payload build workflow in the UI
|
||||
- Added an option for payload types to hook into the `help` function and provide their own as part of their payload type definition
|
||||
- Added a notification to callback tabs if there's new task changes while you're not on the tab
|
||||
- Updated delegate tasks for the same target callback to be grouped together into a single message instead of a separate message per task
|
||||
- Fixed a bug with MythicRPCCallbackSearch where all the timestamps would be 0
|
||||
- Added support for C2 Profiles to include their own agent icons for the UI
|
||||
- Added support for C2 Profiles to report their own SemVer as well
|
||||
- Added support for TranslationContainers to report back their own SemVer
|
||||
- Added support for all consuming containers (eventing, auth, logging, webhook) to report back their own SemVer
|
||||
- Fixed bug in reporting where tasks weren't in order
|
||||
- Updated the CredentialCreateWebhook to support empty credentials
|
||||
- Added callback context menu option for triggering eventing workflows
|
||||
- Also added option to bulk issue eventing workflows from callbacks
|
||||
- Edited workflows so that all can be triggered manually
|
||||
- Edited the server so manul execution can still provide arbitrary extra data, not just keyword execution
|
||||
- Updated operationeventlog to have a dedicated `warning` field that's separate from the level
|
||||
|
||||
## [3.3.1-rc94] - 2025-09-05
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a processing bug for staging_translation messages that were getting their messages overwritten
|
||||
|
||||
## [3.3.1-rc93] - 2025-08-27
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the global setting initializer to properly update from global state
|
||||
|
||||
## [3.3.1-rc92] - 2025-08-27
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a bug where consuming containers were never getting their OnStart called
|
||||
|
||||
## [3.3.1-rc91] - 2025-08-24
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixing bug with eventing causing issues with ints
|
||||
|
||||
## [3.3.1-rc90] - 2025-08-21
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated translation container errors to include missing translation container name
|
||||
- Added new global_setting table
|
||||
- You can now add a default user preferences config that gets applied to all new users
|
||||
- Added new invite_link table
|
||||
- invite links are now persistent across reboots
|
||||
- removed ability to delete invite links
|
||||
- Added new `mythic_parsed_params` field to Task entries
|
||||
- This shows how mythic parses the parameters from the UI
|
||||
- This is viewable from a task, click to "view all parameters"
|
||||
- Fixed some hasura operations to scope down to current operation only
|
||||
- Added new `semver` (semantic version) field to payload types
|
||||
- This will be updated in a later version to fetch data from updated pypi/go packages
|
||||
- Added new TriggerData for the following triggers to allow scoping down to specific commands for specific agents
|
||||
- TaskCreate, TaskStart, TaskFinish, TaskIntercept
|
||||
- This takes a map[string][]string format (a dictionary of string arrays)
|
||||
- The dictionary key is the name of the payload type, the array of strings are the specific commands to trigger on
|
||||
- An empty array of strings means this applies for all commands for that payload type
|
||||
- Added a check in handling messages from agents to check for the all 0 UUID
|
||||
- This is now reported back in the UI as a special error to indicate that the connectivity was a success
|
||||
- This works in tandem with updated C2 Profiles' sample messages to use all 0 UUID values
|
||||
- A new keyword is added for responses, `events`, which is an array where you specify the `keyword` (string) and `data` (dictionary) for an eventing workflow you want to trigger
|
||||
- File Hosting now auto checks and adds `/` if it was somehow missed
|
||||
- Additional checks were added to make removing files easier in case the file was deleted
|
||||
- Updated Eventing response intercept logic to cache existing workflows to reduce the overhead of fetching data for every response
|
||||
- Updated File Hosting tag process to only add tags after C2 Profile returned success for hosting the file
|
||||
- [UI Updates](/MythicReactUI/CHANGELOG.MD#0368---2025-08-21)
|
||||
|
||||
## [3.3.1-rc89] - 2025-08-11
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed the processing of strings/ints for required fields in create tasking eventing
|
||||
|
||||
## [3.3.1-rc88] - 2025-07-31
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a bug in cron eventing that would continue even after error
|
||||
|
||||
## [3.3.1-rc87] - 2025-07-21
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated syncing for payload types to only happen with new c2, not all c2
|
||||
- Updated syncing for translation containers to only force sync payload types when translation container is new
|
||||
|
||||
## [3.3.1-rc86] - 2025-07-18
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the processing of new file tracking to auto mark the file as completed for uploads
|
||||
|
||||
## [3.3.1-rc85] - 2025-07-17
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated processing of command_augment containers to auto add commands to callbacks on any sync
|
||||
|
||||
## [3.3.1-rc84] - 2025-07-07
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the processing for completion functions to not blow away subtask error codes
|
||||
|
||||
## [3.3.1-rc83] - 2025-07-03
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the create credential webhook to more uniquely check for duplicates by type and comment
|
||||
|
||||
## [3.3.1-rc82] - 2025-07-02
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the cookie value to have secure `false` but httpOnly `true` so that `http` only versions of the UI can still access images via cookies
|
||||
|
||||
## [3.3.1-rc81] - 2025-07-01
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the invite link code to have fallback mechanisms for determining the full link invite address
|
||||
|
||||
## [3.3.1-rc80] - 2025-07-01
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated back-end to not try to double-add commands when building payloads and checking dependencies
|
||||
|
||||
## [3.3.1-rc79] - 2025-06-27
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the processing for PushC2 to track the number of connected clients and only mark disconnected when they all exit
|
||||
|
||||
## [3.3.1-rc78] - 2025-06-23
|
||||
|
||||
### Changed
|
||||
|
||||
- Added `payload_id` and `callback_id` as options for tags
|
||||
- Added `supports_multiple_c2_in_build` and `supports_multiple_c2_instances_in_build` database fields for Payload Types
|
||||
- This is prepping for a future update to allow Payload Types to identify if they support multiple c2 to be selected at build
|
||||
|
||||
## [3.3.1-rc77] - 2025-06-19
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the database to track if a task wants to reprocess with a new command or the original command
|
||||
- This was originally updated for `Command Augmentation` containers to allow proper tracking for when
|
||||
- the `CommandName` was changed for a new `CommandPayloadType`, but this also resulted in every time the
|
||||
- `CommandName` was changed in tasking that the processing would continue under the new command, which isn't always ideal
|
||||
- Now, only if you set `response.ReprocessAtNewCommandPayloadType` will the processing change to the new `CommandName`
|
||||
- This means that CommandA that sets its `response.CommandName = commandB` and does a `process_response` field will have the message processed at `CommandA` still
|
||||
- Setting the above AND `response.ReprocessAtNewCommandPayloadType` will have the `process_response` processed at `commandB` (specify the same payload type name is ok too)
|
||||
|
||||
## [3.3.1-rc76] - 2025-06-18
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated processing for agent messages to support new MythicRPCHandleAgentJson function
|
||||
- This allows developers to hook into more "standard" agent message processing from MythicRPC locations
|
||||
- Updated bulk downloading to force host if none exists ("UNKNOWN") and removes trailing "." if no file extension exists
|
||||
- Fixed an issue where sometimes a callback on checking in after getting hidden wouldn't re-appear in the active table
|
||||
|
||||
## [3.3.1-rc75] - 2025-06-11
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the processing of completion handlers for tasks to better account script only and child tasks
|
||||
- Added `cwd` and `impersonation_context` to callbacks as part of their `checkin` and update messages
|
||||
- Added new `callback` field to agent response dictionary
|
||||
- this field allows you to update any fields that you'd normally set in your `checkin` message
|
||||
- Added new field to tasks, `CommandParameterType`, to better track where processing should go if a task sets a new PayloadType as part of its processing
|
||||
- An example would be `forge` updating the command name and container where processing should continue - this new payload type is also tracked
|
||||
- Updated health check code for database to properly report success
|
||||
|
||||
## [3.3.1-rc74] - 2025-06-09
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the processing of commands for building payloads to include `dependencies` in a command's attributes
|
||||
- if `command1` is a dependency for `command2`, and the user selected command2 but not command1 when building,
|
||||
- then mythic will automatically add `command1` to the list of commands that need to be built into the agent
|
||||
- an example would be `dependencies = ["command1"]`
|
||||
|
||||
## [3.3.1-rc73] - 2025-06-04
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the UI to allow creation of new Artifacts (that aren't tied to specific tasks)
|
||||
- Updated payload updating information to a new GraphQL Action for:
|
||||
- description, callback_alert, delete, and callback_allowed
|
||||
- New data tracked for callback_allowed to optionally prevent new callbacks from payloads
|
||||
- Updated Artifact processing
|
||||
- Reporting an artifact will result in getting back an `id` for the artifact
|
||||
- That `id` can then be sent in future artifact messages to update its `needs_cleanup` and `resolved` status
|
||||
- Payloads now have a `callback_allowed` attribute
|
||||
- this is `true` by default, but setting it to false (toggling in the UI on the payloads page or callback metadata) will prevent future new callbacks from that payload
|
||||
- this feature was "implicitly" there when you delete a payload, but that is removed in favor of this new method
|
||||
- A new UI element will show on the payload if it's not marked for allowing callbacks
|
||||
|
||||
## [3.3.1-rc72] - 2025-6-03
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the MythicRPCCallbackUpdate call to allow no specifying C2 Profile when asking to update last checkin time
|
||||
|
||||
## [3.3.1-rc71]- 2025-05-30
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -130,4 +130,49 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
SOFTWARE.
|
||||
|
||||
MIT license
|
||||
===========
|
||||
|
||||
Copyright (c) 2017 sql.js authors (see AUTHORS)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
|
||||
# Some portions of the Makefile taken from:
|
||||
Copyright 2017 Ryusei Yamaguchi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -4,6 +4,542 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.3.113] - 2026-07-01
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a bug in the process browser that would set a filter to undefined instead of ""
|
||||
|
||||
## [0.3.112] - 2026-05-17
|
||||
|
||||
- Updated the single task view to show interactive tasking surrounding results
|
||||
|
||||
## [0.3.111] - 2026-05-07
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue with useLazyQuery spamming requests
|
||||
|
||||
## [0.3.110] - 2026-04-16
|
||||
|
||||
### Changed
|
||||
|
||||
- Added more generic MythicPageBody and MythicServerHeader components to wrap most pages/headers
|
||||
- Added a new border color you can select for borders of a few components
|
||||
- Updated a few places with hard-coded px values to more scalable rem values
|
||||
|
||||
## [0.3.109] - 2026-04-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the UI to sort c2 profile parameters during payload build by ui_position if specified
|
||||
|
||||
## [0.3.107] - 2026-03-23
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed typo in file browser for remove_folder
|
||||
|
||||
## [0.3.106] - 2026-03-21
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue with clicking file uploads in modals
|
||||
|
||||
## [0.3.105] - 2026-03-20
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated some of the file upload pieces in the UI to support drag-and-drop
|
||||
- Added an `autoScroll` button to interactive tasking so you can force auto scroll or not
|
||||
- Fixed some of the drag and drop modal components
|
||||
|
||||
## [0.3.104] - 2026-03-19
|
||||
|
||||
### Changed
|
||||
|
||||
- Added ability to drag and move some modals and copy data from behind them
|
||||
|
||||
## [0.3.103] - 2026-03-17
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed issue with file uploads for payload/wrapper builds when starting from existing payload
|
||||
|
||||
## [0.3.102] - 2026-03-12
|
||||
|
||||
### Changed
|
||||
|
||||
- Slight tweak to `tabs` browser script capability to cap the max size of the tabs
|
||||
- Fixed a scrolling issue on the single task view page
|
||||
|
||||
## [0.3.101] - 2026-02-19
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue with database view in split view tasking that would cause overflow
|
||||
|
||||
## [0.3.100] - 2026-02-18
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the eventing page to only stream in updates that match the current filter
|
||||
- Updated payload parameter array type to start empty
|
||||
|
||||
## [0.3.99] - 2026-02-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated process_browser and file_browser tables to allow column sorting, hiding, and saved to user preferences
|
||||
- Updated process_browser to have a new column for integrity_level
|
||||
|
||||
## [0.3.98] - 2026-02-08
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the default shortcuts to remove Reporting and add CreatePayload
|
||||
- Updated Task Search on Parameters to include agent parameters, not just original and display
|
||||
- Updated callback table bulk actions to include a note about callback last checkin times not updates in those windows
|
||||
|
||||
## [0.3.97] - 2026-02-02
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated browser script tables to rever sort after toggling ASC/DESC to match other virtualized tables in Mythic
|
||||
|
||||
## [0.3.96] - 2026-01-30
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the file browser to support `file_browser:remove` and `file_browser:remove_folder`
|
||||
- this allows separate commands for deleting files vs deleting folders (or the same command if you want)
|
||||
|
||||
## [0.3.95] - 2026-01-29
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated tasking data for browser scripts to also provide callback's pid
|
||||
- Updated browser scripting to provide new type: `tabs`
|
||||
- returning `{"tabs": [ {"title": "tab title here", "content": {"table": {table dict} } } ] }`
|
||||
- this creates one tab with title `tab title here` that renders a table
|
||||
|
||||
## [0.3.94] - 2026-01-27
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a UI caching bug in split tasking view that would hide tasks on clicking previously issued tasks
|
||||
|
||||
## [0.3.93] - 2026-01-14
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated UI to allow output background color and output text color to be set separate from the "theme"
|
||||
- Updated UI to reflect file size updates from file browser while streaming
|
||||
|
||||
## [0.3.92] - 2026-01-09
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a bug in dynamic query parameters in the modal that would sometimes error on choices
|
||||
|
||||
## [0.3.91] - 2026-01-08
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a bug where c2 parameter deviations would try to iterate over null
|
||||
|
||||
## [0.3.90] - 2026-01-01
|
||||
|
||||
### Changed
|
||||
|
||||
- Added a new modal to the UI when looking at a payload's commands to get insight into how to task them via scripting
|
||||
|
||||
## [0.3.89] - 2025-12-31
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an alignment issue in the UI for split callbacks that would cause a horizontal scroll bar
|
||||
- Fixed a height issue on the browser script page that wasn't going the full page height
|
||||
- Added an option to string dialog boxes to wrap the displayed content or not
|
||||
|
||||
## [0.3.88] - 2025-12-29
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a bug where payload creation wouldn't properly show "False" settings from previously used configs
|
||||
- Fixed a few instances of double scrollbar in modals
|
||||
|
||||
## [0.3.87] - 2025-12-18
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the file media viewer to also fetch updates more dynamically
|
||||
|
||||
## [0.3.85] - 2025-12-15
|
||||
|
||||
### Changed
|
||||
|
||||
- This updates the default tasking view to split tasking
|
||||
- This adds the task to the top of the task output in the split tasking view for easier tracking
|
||||
|
||||
## [0.3.84] - 2025-12-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an ordering issues in backgrounds that got rid of the side bar colors in some browsers
|
||||
|
||||
## [0.3.83] - 2025-12-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated context menu for virtualized tables
|
||||
- Updated column adjustment icon for virtualized tables to be || instead of three vertical dots
|
||||
- Updated virtualized table header context menu display type
|
||||
- Updated minimum column width for virtualized columns to be 100 instead of 20
|
||||
- Updated virtualized tree views to check item.has_children instead of item.metadata.has_children
|
||||
- this is in support of custom browsers
|
||||
- Updated context menu for callbacks to display custom browser support
|
||||
- Updated callback tabs to have customFileBasedBrowser type for new custom browser support
|
||||
- Added custom browser table and tree views
|
||||
- Updated file browser to come more inline with custom browsers
|
||||
- Updated context menu for virtualized tree to have some copy options
|
||||
- Fixed an issue with `\\` in tasking on the command line when not surrounded by quotes
|
||||
- Updated token select in browser views to not be so squished
|
||||
- Updated split tasking view to align content
|
||||
- Added new `database` view for file previews with sql.js and wasm
|
||||
- Updated `icon` usage in browser scripts to support just supplying the font awesome name instead of a curated list
|
||||
- https://fontawesome.com/v6/search?s=solid&ic=free-collection all of these should be available
|
||||
- Updated task display to allow showing of operator names that bypassed OPSEC checks
|
||||
- Updated task buttons for split view to be along the right-hand side instead of a floating speeddial button at the bottom
|
||||
- Added verifier_regex to command parameters
|
||||
- Fixed a UI bug where display names of command parameter descriptions/names would overflow
|
||||
- Added support for broader range of hide conditions in payload type build parameters
|
||||
- Fixed a UI bug where payload selection options for wrappers weren't all sorted by time
|
||||
- Updated Eventing Instance Table view to be more in line with other virtualized tables
|
||||
- Updated view to show "selected" eventing workflow and task in split view tasking to be easier to see
|
||||
- Updated detailed payload views to show parameter names and descriptions, just like the building views
|
||||
- Updated hovering over payloads to show payload type semver when payload was created vs currently installed version
|
||||
- Added CustomBrowser entry to the "Installed Services" page
|
||||
- Added CustomBrowser search page
|
||||
- Fixed Task/Callback links on Token search page
|
||||
- Updated UI Configuration settings to support `showOPSECBypassUsername` and new `navBarBottomColor`
|
||||
- the latter allows you to have gradients for the side navbar instead of just a solid color
|
||||
|
||||
## [0.3.82] - 2025-11-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue with creating payloads without including any C2 profiles
|
||||
|
||||
## [0.3.81] - 2025-10-29
|
||||
|
||||
### Changed
|
||||
|
||||
- Updating event log message default display state
|
||||
|
||||
## [0.3.80] - 2025-10-28
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a bug where hidden values would still get sent to build process for payloads
|
||||
|
||||
## [0.3.79] - 2025-10-28
|
||||
|
||||
### Changed
|
||||
|
||||
- Added new UiPosition and DynamicQueryFunction capabilities to the payload build process
|
||||
- Added new filemeta "copy_of_file" attribute to be shown on file search pages
|
||||
|
||||
## [0.3.78] - 2025-10-16
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue with previous payload selection in payload creation
|
||||
|
||||
## [0.3.77] - 205-10-16
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a bug with cli parsing with quotes for parameters
|
||||
- Fixing an issue with file browser parameter arrays treated like dictionaries
|
||||
|
||||
## [0.3.76] - 2025-10-16
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed the process for filter by command attribute in task modals
|
||||
|
||||
## [0.3.75] - 2025-10-13
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a bug with payload type's c2 variations for dictionaries wasn't getting picked up by default
|
||||
|
||||
## [0.3.74] - 2025-10-11
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue with displayed boolean values always showing true for payload configs
|
||||
- Fixed an issue with files from pre-configured payloads when generating new payloads
|
||||
|
||||
## [0.3.73] - 2025-10-09
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue with linking modal displaying too many alerts and not tracking new payloads initially
|
||||
|
||||
## [0.3.72] - 2025-10-08
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a "cancel" button not closing a modal for eventing wizard
|
||||
- Fixed an issue with `File` types showing links for files not uploaded yet when building payloads
|
||||
|
||||
## [0.3.71] - 2025-10-07
|
||||
|
||||
### Changed
|
||||
|
||||
- updated the position of the c2 profile icons on the graph views
|
||||
|
||||
## [0.3.70] - 2025-10-07
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the Payload and Wrapper creation pages
|
||||
- Added support for PayloadType new fields:
|
||||
- c2_parameter_deviations: ability to set choices, default_value for c2-specific fields
|
||||
- supports_multiple_c2_in_build: ability to signify if more than one c2 is allowed or not
|
||||
- supports_multiple_c2_instances_in_build: ability to signify if more than one instance of a specific 2 is allowed or not (i.e. 2 HTTPX profiles)
|
||||
- semver: display a semantic version for Payload Types if set
|
||||
|
||||
## [0.3.69] - 2025-08-21
|
||||
|
||||
- Fixed an issue with browser script tables getting hidden by the container
|
||||
|
||||
## [0.3.68] - 2025-08-21
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated browser script tables to have filter options again
|
||||
- Updated browser script tables to have option to copy out entire table as JSON, CSV, or TSV
|
||||
- Updated browser script tables to have option to copy out entire table as Pretty Printed Text
|
||||
- Updated task's "View All Parameters" option to show the new mythic_parsed_parameters field
|
||||
- Updated UI to support new map[string][]array format needed by new eventing trigger data
|
||||
- Updated eventing stepper UI wizard to support new trigger data
|
||||
- Updated file hosting dialog to sort available c2 profiles by name
|
||||
- Updated file search pages to remove entries completely if they're deleted
|
||||
- Updated invite links and global settings to use new database focused data
|
||||
|
||||
## [0.3.67] - 2025-08-11
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the timer for checking jwt remaining time to only happen if you're logged in
|
||||
- Updated
|
||||
## [0.3.66] - 2025-08-04
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the processing of the UI creation for eventing to process numbers vs strings better
|
||||
|
||||
## [0.3.65] - 2025-08-01
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated preferences to always fetch and not get cached
|
||||
|
||||
## [0.3.64] - 2025-07-30
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the task parameter search to also include display_params, not just original_params
|
||||
|
||||
## [0.3.63] - 2025-07-29
|
||||
|
||||
### Changed
|
||||
|
||||
- Added the `pid` field to the callback search table and the ability to search by pid
|
||||
|
||||
## [0.3.62] - 2025-07-28
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the webhook/auth buttons to properly check for subscription / online status for tests
|
||||
|
||||
## [0.3.61] - 2025-07-25
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue with image tiling getting reset on changes
|
||||
|
||||
## [0.3.59] - 2025-07-16
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated some of the search pages to better account for changing search options
|
||||
- Fixed the callback group search for tasks
|
||||
|
||||
## [0.3.58] - 2025-07-16
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated some of the context in tag search for tasks to include more callback information
|
||||
- Added fields for Callback/Payload tags to show up
|
||||
|
||||
## [0.3.57] - 2025-07-13
|
||||
|
||||
### Changed
|
||||
|
||||
- Added the option to show more fields right above the tasking bar
|
||||
|
||||
## [0.3.56] - 2025-07-11
|
||||
|
||||
### Changed
|
||||
|
||||
- Added the cursor view back to ace text fields
|
||||
- Updated the menu dropdown button to not fade into colored rows as much
|
||||
|
||||
## [0.3.55] - 2025-07-03
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated task dialog to allow removal of credentials
|
||||
- Updated task dialog on ambiguous parameters to open modal
|
||||
- Updated task dialog to sort credentials most recent at the top to oldest at the bottom
|
||||
|
||||
## [0.3.54] - 2025-07-01
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the cooking parsing to trim fields
|
||||
|
||||
## [0.3.53] - 2025-06-30
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a bug with updating a user's email in the UI
|
||||
|
||||
## [0.3.52] - 2025-06-29
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed some alignment issues in the search page
|
||||
- Added an option when searching tasks to filter by operator
|
||||
- Added the ability to click to zoom into rendered media images
|
||||
|
||||
## [0.3.51] - 2025-06-25
|
||||
|
||||
### Changed
|
||||
|
||||
- Added a payload diff capability via the Actions dropdown on any payload
|
||||
|
||||
## [0.3.50] - 2025-06-24
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the single task view page to not show console view
|
||||
- Merged in PR with a few slight tweaks for console view
|
||||
|
||||
## [0.3.49] - 2025-06-23
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the UI to leverage the new payload/callback tagging capabilities
|
||||
|
||||
## [0.3.48] - 2025-06-19
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed an issue with cli parsing where JavaScript would think `1` is a valid JSON string
|
||||
|
||||
## [0.3.46] - 2025-06-19
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the task modals to show the name and description at once
|
||||
|
||||
## [0.3.45] - 2025-06-17
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated tasking field to show more info about cwd and impersonation_context
|
||||
- Added user setting to show/hide new cwd and impersonation_context tabs
|
||||
- Added new tab for extra_info field in callbacks to show
|
||||
- Added ability to color cwd, impersonation context, and extra info separately
|
||||
- Added ability to generate new payload based on slightly edited config from the payload's Action dropdown
|
||||
|
||||
## [0.3.44] - 2025-06-15
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the console view to have a output divider
|
||||
- Updated the tasking prompt color to be a separate color you can specify in your settings
|
||||
- Updated task/callback links to match the tasking prompt color so they blend better
|
||||
- Updated the highlighting of text fields to be consistent grey instead of based on the secondary color
|
||||
- should help with some whiplash from editing colors
|
||||
- Updated the time formatting for console view
|
||||
- Update the command selection pane when building payloads to not show commands you can't include
|
||||
|
||||
## [0.3.43] - 2025-06-13
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the background image to not tile
|
||||
|
||||
## [0.3.42] - 2025-06-12
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the way that the "Console" view works for tasking
|
||||
- Added `cwd` and `impersonation_context` to UI for callbacks
|
||||
|
||||
## [0.3.41] - 2025-06-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Added more safeguards when importing theme configs to make sure there's valid values
|
||||
|
||||
## [0.3.40] - 2025-06-09
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the default value for blank arrays to be `[]` instead of `[""]`
|
||||
|
||||
## [0.3.39] - 2025-06-06
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed some rounded corners on the dashboards page
|
||||
- Updated the Payload Types / C2 Profiles page to have some context if there's no container installed of each type
|
||||
- Fixed some hasura permissions that were not scoping fetches down to the current operation but all operations you were assigned to
|
||||
|
||||
## [0.3.38] - 2025-06-04
|
||||
|
||||
### Changed
|
||||
|
||||
- Added new button in Artifacts search page to add new artifacts
|
||||
|
||||
## [0.3.37] - 2025-06-03
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed bug with viewing files for eventing workflows
|
||||
- Updated comment view on credential types for tasking
|
||||
- Updated resize bars to be a bit thinner so that their color is less intrustive
|
||||
- Updated the default dark-mode color for the side bar to be a bit darker
|
||||
|
||||
## [0.3.36] - 2025-06-02
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a bug with double-clicking column headers for the active callbacks page that would break while saving preferences
|
||||
|
||||
## [0.3.35] - 2025-05-30
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed the spacing in the payloads table to not have each row extra large when not full
|
||||
|
||||
## [0.3.34] - 2025-05-30
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
const webpack = require('webpack');
|
||||
module.exports = function override(config, env) {
|
||||
config.resolve.fallback = {
|
||||
//url: require.resolve('url'),
|
||||
fs: false,
|
||||
assert: require.resolve('assert'),
|
||||
crypto: require.resolve('crypto-browserify'),
|
||||
path: require.resolve('path-browserify'),
|
||||
//http: require.resolve('stream-http'),
|
||||
//https: require.resolve('https-browserify'),
|
||||
//os: require.resolve('os-browserify/browser'),
|
||||
//buffer: require.resolve('buffer'),
|
||||
stream: require.resolve('stream-browserify'),
|
||||
};
|
||||
/*
|
||||
config.plugins.push(
|
||||
new webpack.ProvidePlugin({
|
||||
process: 'process/browser',
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
);
|
||||
|
||||
*/
|
||||
|
||||
return config;
|
||||
}
|
||||
Generated
+934
-287
File diff suppressed because it is too large.
Load diff
@@ -6,7 +6,11 @@
|
||||
"author": "@its_a_feature_",
|
||||
"description": "React UI for Mythic C2 Framework",
|
||||
"devDependencies": {
|
||||
"react-scripts": "^5.0.1"
|
||||
"buffer": "^6.0.3",
|
||||
"crypto-browserify": "^3.12.1",
|
||||
"react-app-rewired": "^2.2.1",
|
||||
"react-scripts": "^5.0.1",
|
||||
"stream-browserify": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.13.8",
|
||||
@@ -38,6 +42,7 @@
|
||||
"jwt-decode": "^4.0.0",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"path-browserify": "^1.0.1",
|
||||
"randexp": "^0.5.3",
|
||||
"react": "^18.3.1",
|
||||
"react-ace": "^14.0.1",
|
||||
@@ -45,7 +50,7 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-moment": "^1.1.3",
|
||||
"react-router-dom": "^7.5.2",
|
||||
"react-router-dom": "^7.13.1",
|
||||
"react-scrollbar-size": "^5.0.0",
|
||||
"react-split": "^2.0.14",
|
||||
"react-toastify": "^11.0.5",
|
||||
@@ -55,18 +60,19 @@
|
||||
"react-window": "^1.8.11",
|
||||
"save-svg-as-png": "^1.4.17",
|
||||
"semver": "^7.6.2",
|
||||
"sql.js": "^1.13.0",
|
||||
"styled-components": "^6.1.17",
|
||||
"uuid": "^9.0.1",
|
||||
"webpack": "^5.99.6",
|
||||
"webpack": "^5.105.0",
|
||||
"word-wrap": "^1.2.5"
|
||||
},
|
||||
"scripts": {
|
||||
"react-start": "CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=true WDS_POLLING=true BROWSER=none PORT=3000 WDS_SOCKET_PORT=7443 WDS_SOCKET_PATH=ws react-scripts start",
|
||||
"react-build": "react-scripts build",
|
||||
"react-test": "react-scripts test",
|
||||
"react-start": "CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=true WDS_POLLING=true BROWSER=none PORT=3000 WDS_SOCKET_PORT=7443 WDS_SOCKET_PATH=ws react-app-rewired start",
|
||||
"react-build": "react-app-rewired build",
|
||||
"react-test": "react-app-rewired test",
|
||||
"react-eject": "react-scripts eject",
|
||||
"build": "npm run react-build",
|
||||
"start": "cross-env BROWSER=none CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=true WDS_POLLING=true PORT=3000 WDS_SOCKET_PORT=7443 WDS_SOCKET_PATH=ws npm run react-start"
|
||||
"start": "BROWSER=none CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=true WDS_POLLING=true PORT=3000 WDS_SOCKET_PORT=7443 WDS_SOCKET_PATH=ws npm run react-start"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
|
||||
+63
-17
@@ -18,6 +18,12 @@ export const taskTimestampDisplayFieldOptions = [
|
||||
display: "When Agent Picked up Task",
|
||||
}
|
||||
]
|
||||
export const taskingContextFieldsOptions = ["impersonation_context", "cwd", "user", "host", "ip", "pid", "process_short_name", "extra_info", "architecture"].sort();
|
||||
export const defaultShortcuts = [
|
||||
"ActiveCallbacks", "Payloads", "PayloadTypesAndC2",
|
||||
"Operations", "SearchFiles", "SearchProxies",
|
||||
"CreatePayload", "Eventing",
|
||||
].sort();
|
||||
export const operatorSettingDefaults = {
|
||||
fontSize: 12,
|
||||
navBarOpen: false,
|
||||
@@ -26,26 +32,30 @@ export const operatorSettingDefaults = {
|
||||
hideUsernames: false,
|
||||
showIP: false,
|
||||
showHostname: false,
|
||||
showOPSECBypassUsername: false,
|
||||
showCallbackGroups: false,
|
||||
useDisplayParamsForCLIHistory: true,
|
||||
interactType: "interact",
|
||||
interactType: "interactSplit",
|
||||
taskTimestampDisplayField: "timestamp",
|
||||
callbacks_table_columns: ["Interact", "Host", "Domain", "User", "Description", "Last Checkin", "Agent", "IP", "PID"],
|
||||
callbacks_table_filters: {},
|
||||
autoTaskLsOnEmptyDirectories: false,
|
||||
hideBrowserTasking: false,
|
||||
["experiment-responseStreamLimit"]: 50,
|
||||
hideTaskingContext: false,
|
||||
taskingContextFields: ["impersonation_context", "cwd"],
|
||||
["experiment-responseStreamLimit"]: 200,
|
||||
sideShortcuts: defaultShortcuts,
|
||||
palette: {
|
||||
primary: {
|
||||
dark: "#75859b",
|
||||
light: "#75859b",
|
||||
},
|
||||
error: {
|
||||
dark: '#da3237',
|
||||
dark: '#bd5142',
|
||||
light: '#c42c32'
|
||||
},
|
||||
success: {
|
||||
dark: '#44b636',
|
||||
dark: '#85b089',
|
||||
light: '#0e7004',
|
||||
},
|
||||
secondary: {
|
||||
@@ -53,19 +63,19 @@ export const operatorSettingDefaults = {
|
||||
light: '#a6a5a5'
|
||||
},
|
||||
info: {
|
||||
dark: '#2184d3',
|
||||
dark: '#84b4dc',
|
||||
light: '#4990b2'
|
||||
},
|
||||
warning: {
|
||||
dark: "#f57c00",
|
||||
dark: "#dc8455",
|
||||
light: "#ffb74d",
|
||||
},
|
||||
background: {
|
||||
dark: '#303030',
|
||||
dark: '#282828',
|
||||
light: '#f6f6f6'
|
||||
},
|
||||
paper: {
|
||||
dark: '#424242',
|
||||
dark: '#282828',
|
||||
light: '#ececec'
|
||||
},
|
||||
tableHeader: {
|
||||
@@ -77,11 +87,11 @@ export const operatorSettingDefaults = {
|
||||
light: "#e8e8e8",
|
||||
},
|
||||
pageHeader: {
|
||||
dark: '#706c6e',
|
||||
dark: '#1b2025',
|
||||
light: '#706c6e'
|
||||
},
|
||||
text: {
|
||||
dark: "#ffffff",
|
||||
dark: "#e4e4e4",
|
||||
light: "#000000",
|
||||
},
|
||||
selectedCallbackColor: {
|
||||
@@ -105,16 +115,52 @@ export const operatorSettingDefaults = {
|
||||
light: '#ffffff'
|
||||
},
|
||||
navBarColor: {
|
||||
dark: "#3c4d67",
|
||||
light: "#3c4d67",
|
||||
dark: "#194573",
|
||||
light: "#3b606d",
|
||||
},
|
||||
navBarBottomColor: {
|
||||
dark: "#330814",
|
||||
light: "#283581",
|
||||
},
|
||||
taskPromptTextColor: {
|
||||
dark: '#bebebe',
|
||||
light: '#a6a5a5'
|
||||
},
|
||||
taskPromptCommandTextColor: {
|
||||
dark: "#e4e4e4",
|
||||
light: "#000000",
|
||||
},
|
||||
taskContextColor: {
|
||||
dark: "#122848",
|
||||
light: "#acc0da",
|
||||
},
|
||||
taskContextImpersonationColor: {
|
||||
dark: "#641616",
|
||||
light: "#dec0c0",
|
||||
},
|
||||
taskContextExtraColor: {
|
||||
dark: "#2a5953",
|
||||
light: "#a7ce9d",
|
||||
},
|
||||
emptyFolderColor: {
|
||||
dark: '#bebebe',
|
||||
light: '#a6a5a5'
|
||||
},
|
||||
outputBackgroundColor: {
|
||||
dark: '#282828',
|
||||
light: '#f6f6f6'
|
||||
},
|
||||
outputTextColor: {
|
||||
dark: '#f6f6f6',
|
||||
light: '#282828',
|
||||
},
|
||||
borderColor: {
|
||||
dark: "#595858",
|
||||
light: "#c5c5c5"
|
||||
}
|
||||
},
|
||||
}
|
||||
export const defaultShortcuts = [
|
||||
"ActiveCallbacks", "Payloads", "PayloadTypesAndC2",
|
||||
"Operations", "SearchFiles", "SearchProxies",
|
||||
"Reporting", "Eventing",
|
||||
].sort();
|
||||
|
||||
export const mePreferences = makeVar(operatorSettingDefaults);
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import { Home } from './pages/Home/Home';
|
||||
import { LoggedInRoute } from './utilities/LoggedInRoute';
|
||||
import { Callbacks } from './pages/Callbacks/Callbacks';
|
||||
import { Search } from './pages/Search/Search';
|
||||
import { ConsumingServices } from './pages/ConsumingServices/ConsumingServices';
|
||||
import React, {createContext} from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
import { useReactiveVar } from '@apollo/client';
|
||||
@@ -39,7 +38,20 @@ import {Eventing} from "./pages/Eventing/Eventing";
|
||||
import {InviteForm} from "./pages/Login/InviteForm";
|
||||
import {snackActions} from "./utilities/Snackbar";
|
||||
import {TopAppBarVertical} from "./TopAppBarVertical";
|
||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||
import * as Icons from '@fortawesome/free-solid-svg-icons';
|
||||
import {Jupyter} from "./pages/Jupyter/Jupyter";
|
||||
import {Hasura} from "./pages/Hasura/Hasura";
|
||||
|
||||
// add all fas icons
|
||||
const iconList = Object
|
||||
.keys(Icons)
|
||||
.filter(key => key !== "fas" && key !== "prefix" )
|
||||
.map(icon => Icons[icon])
|
||||
|
||||
library.add(...iconList)
|
||||
|
||||
export const MeContext = createContext({});
|
||||
export const userSettingsQuery = gql`
|
||||
query getUserSettings {
|
||||
getOperatorPreferences {
|
||||
@@ -57,90 +69,204 @@ export function App(props) {
|
||||
const [loadingPreference, setLoadingPreferences] = React.useState(true);
|
||||
const [themeMode, themeToggler] = useDarkMode();
|
||||
const theme = React.useMemo(
|
||||
() =>
|
||||
createTheme({
|
||||
transitions: {
|
||||
// So we have `transition: none;` everywhere
|
||||
create: () => 'none',
|
||||
},
|
||||
palette: {
|
||||
contrastThreshold: 4.5,
|
||||
//tonalOffset: 0.5,
|
||||
primary: {
|
||||
main: themeMode === "dark" ? preferences?.palette?.primary?.dark || operatorSettingDefaults.palette.primary.dark :
|
||||
preferences?.palette?.primary?.light || operatorSettingDefaults.palette.primary.light,
|
||||
() => {
|
||||
try{
|
||||
return createTheme({
|
||||
transitions: {
|
||||
// So we have `transition: none;` everywhere
|
||||
create: () => 'none',
|
||||
},
|
||||
error: {
|
||||
main: themeMode === "dark" ? preferences?.palette?.error?.dark || operatorSettingDefaults.palette.error.dark :
|
||||
preferences?.palette?.error?.light || operatorSettingDefaults.palette.error.light,
|
||||
palette: {
|
||||
contrastThreshold: 4.5,
|
||||
//tonalOffset: 0.5,
|
||||
primary: {
|
||||
main: themeMode === "dark" ? preferences?.palette?.primary?.dark || operatorSettingDefaults.palette.primary.dark :
|
||||
preferences?.palette?.primary?.light || operatorSettingDefaults.palette.primary.light,
|
||||
},
|
||||
error: {
|
||||
main: themeMode === "dark" ? preferences?.palette?.error?.dark || operatorSettingDefaults.palette.error.dark :
|
||||
preferences?.palette?.error?.light || operatorSettingDefaults.palette.error.light,
|
||||
},
|
||||
success: {
|
||||
main: themeMode === 'dark' ? preferences?.palette?.success?.dark || operatorSettingDefaults.palette.success.dark :
|
||||
preferences?.palette?.success?.light || operatorSettingDefaults.palette.success.light,
|
||||
},
|
||||
secondary: {
|
||||
main: themeMode === 'dark' ? preferences?.palette?.secondary?.dark || operatorSettingDefaults.palette.secondary.dark :
|
||||
preferences?.palette?.secondary?.light || operatorSettingDefaults.palette.secondary.light,
|
||||
},
|
||||
info: {
|
||||
main: themeMode === 'dark' ? preferences?.palette?.info?.dark || operatorSettingDefaults.palette.info.dark :
|
||||
preferences?.palette?.info?.light || operatorSettingDefaults.palette.info.light,
|
||||
},
|
||||
warning: {
|
||||
main: themeMode === 'dark' ? preferences?.palette?.warning?.dark || operatorSettingDefaults.palette.warning.dark :
|
||||
preferences?.palette?.warning?.light || operatorSettingDefaults.palette.warning.light,
|
||||
},
|
||||
mode: themeMode,
|
||||
background: {
|
||||
contrast: themeMode === 'dark' ? preferences?.palette?.background?.light || operatorSettingDefaults.palette.background.light :
|
||||
preferences?.palette?.background?.dark || operatorSettingDefaults.palette.background.dark,
|
||||
default: themeMode === "dark" ? preferences?.palette?.background?.dark || operatorSettingDefaults.palette.background.dark :
|
||||
preferences?.palette?.background?.light || operatorSettingDefaults.palette.background.light,
|
||||
paper: themeMode === "dark" ? preferences?.palette?.paper?.dark || operatorSettingDefaults.palette.paper.dark :
|
||||
preferences?.palette?.paper?.light || operatorSettingDefaults.palette.paper.light,
|
||||
image: themeMode === "dark" ? preferences?.palette?.backgroundImage?.dark || operatorSettingDefaults.palette.backgroundImage.dark :
|
||||
preferences?.palette?.backgroundImage?.light || operatorSettingDefaults.palette.backgroundImage.light,
|
||||
},
|
||||
text: {
|
||||
primary: themeMode === 'dark' ? preferences?.palette?.text?.dark || operatorSettingDefaults.palette.text.dark :
|
||||
preferences?.palette?.text?.light || operatorSettingDefaults.palette.text.light,
|
||||
contrast: themeMode === 'dark' ? '#000' : '#fff',
|
||||
},
|
||||
graphGroupRGBA: themeMode === 'dark' ? 'rgba(57, 76, 93, 0.5)' : 'rgba(211, 215, 232, 0.5)',
|
||||
speedDialAction: themeMode === 'dark' ? '#495054' : '#ffffff',
|
||||
},
|
||||
success: {
|
||||
main: themeMode === 'dark' ? preferences?.palette?.success?.dark || operatorSettingDefaults.palette.success.dark :
|
||||
preferences?.palette?.success?.light || operatorSettingDefaults.palette.success.light,
|
||||
folderColor: '#f1d592',
|
||||
tableHeader: themeMode === 'dark' ? preferences?.palette?.tableHeader?.dark || operatorSettingDefaults.palette.tableHeader.dark :
|
||||
preferences?.palette?.tableHeader?.light || operatorSettingDefaults.palette.tableHeader.light,
|
||||
selectedCallbackColor: themeMode === 'dark' ? preferences?.palette?.selectedCallbackColor?.dark || operatorSettingDefaults.palette.selectedCallbackColor.dark :
|
||||
preferences?.palette?.selectedCallbackColor?.light || operatorSettingDefaults.palette.selectedCallbackColor.light,
|
||||
selectedCallbackHierarchyColor: themeMode === 'dark' ? preferences?.palette?.selectedCallbackHierarchyColor?.dark || operatorSettingDefaults.palette.selectedCallbackHierarchyColor.dark :
|
||||
preferences?.palette?.selectedCallbackHierarchyColor?.light || operatorSettingDefaults.palette.selectedCallbackHierarchyColor.light,
|
||||
tableHover: themeMode === 'dark' ? preferences?.palette?.tableHover?.dark || operatorSettingDefaults.palette.tableHover.dark :
|
||||
preferences?.palette?.tableHover?.light || operatorSettingDefaults.palette.tableHover.light,
|
||||
navBarTextIconColor: themeMode === 'dark' ? preferences?.palette?.navBarIcons?.dark || operatorSettingDefaults.palette.navBarIcons.dark :
|
||||
preferences?.palette?.navBarIcons?.light || operatorSettingDefaults.palette.navBarIcons.light,
|
||||
navBarTextColor: themeMode === 'dark' ? preferences?.palette?.navBarText?.dark || operatorSettingDefaults.palette.navBarText.dark :
|
||||
preferences?.palette?.navBarText?.light || operatorSettingDefaults.palette.navBarText.light,
|
||||
pageHeader: {
|
||||
main: themeMode === 'dark' ? preferences?.palette?.pageHeader?.dark || operatorSettingDefaults.palette.pageHeader.dark :
|
||||
preferences?.palette?.pageHeader?.light || operatorSettingDefaults.palette.pageHeader.light,
|
||||
},
|
||||
secondary: {
|
||||
main: themeMode === 'dark' ? preferences?.palette?.secondary?.dark || operatorSettingDefaults.palette.secondary.dark :
|
||||
preferences?.palette?.secondary?.light || operatorSettingDefaults.palette.secondary.light,
|
||||
pageHeaderText: {
|
||||
main: 'white',
|
||||
},
|
||||
info: {
|
||||
main: themeMode === 'dark' ? preferences?.palette?.info?.dark || operatorSettingDefaults.palette.info.dark :
|
||||
preferences?.palette?.info?.light || operatorSettingDefaults.palette.info.light,
|
||||
topAppBarColor: themeMode === 'dark' ? (preferences?.palette?.navBarColor?.dark || operatorSettingDefaults.palette.navBarColor.dark) :
|
||||
(preferences?.palette?.navBarColor?.light || operatorSettingDefaults.palette.navBarColor.light),
|
||||
topAppBarBottomColor: themeMode === 'dark' ? preferences?.palette?.navBarBottomColor?.dark || operatorSettingDefaults.palette.navBarBottomColor.dark :
|
||||
preferences?.palette?.navBarBottomColor?.light || operatorSettingDefaults.palette.navBarBottomColor.light,
|
||||
typography: {
|
||||
fontSize: 12, //preferences?.fontSize,
|
||||
fontFamily: preferences?.fontFamily
|
||||
},
|
||||
warning: {
|
||||
main: themeMode === 'dark' ? preferences?.palette?.warning?.dark || operatorSettingDefaults.palette.warning.dark :
|
||||
preferences?.palette?.warning?.light || operatorSettingDefaults.palette.warning.light,
|
||||
taskPromptTextColor: themeMode === 'dark' ? preferences?.palette?.taskPromptTextColor?.dark || operatorSettingDefaults.palette.taskPromptTextColor.dark :
|
||||
preferences?.palette?.taskPromptTextColor?.light || operatorSettingDefaults.palette.taskPromptTextColor.light,
|
||||
taskPromptCommandTextColor: themeMode === 'dark' ? preferences?.palette?.taskPromptCommandTextColor?.dark || operatorSettingDefaults.palette.taskPromptCommandTextColor.dark :
|
||||
preferences?.palette?.taskPromptCommandTextColor?.light || operatorSettingDefaults.palette.taskPromptCommandTextColor.light,
|
||||
taskContextColor: themeMode === 'dark' ? preferences?.palette?.taskContextColor?.dark || operatorSettingDefaults.palette.taskContextColor.dark :
|
||||
preferences?.palette?.taskContextColor?.light || operatorSettingDefaults.palette.taskContextColor.light,
|
||||
taskContextImpersonationColor: themeMode === 'dark' ? preferences?.palette?.taskContextImpersonationColor?.dark || operatorSettingDefaults.palette.taskContextImpersonationColor.dark :
|
||||
preferences?.palette?.taskContextImpersonationColor?.light || operatorSettingDefaults.palette.taskContextImpersonationColor.light,
|
||||
taskContextExtraColor: themeMode === 'dark' ? preferences?.palette?.taskContextExtraColor?.dark || operatorSettingDefaults.palette.taskContextExtraColor.dark :
|
||||
preferences?.palette?.taskContextExtraColor?.light || operatorSettingDefaults.palette.taskContextExtraColor.light,
|
||||
emptyFolderColor: themeMode === 'dark' ? preferences?.palette?.emptyFolderColor?.dark || operatorSettingDefaults.palette.emptyFolderColor.dark :
|
||||
preferences?.palette?.emptyFolderColor?.light || operatorSettingDefaults.palette.emptyFolderColor.light,
|
||||
outputBackgroundColor: themeMode === 'dark' ? preferences?.palette?.outputBackgroundColor?.dark || operatorSettingDefaults.palette.outputBackgroundColor.dark :
|
||||
preferences?.palette?.outputBackgroundColor?.light || operatorSettingDefaults.palette.outputBackgroundColor.light,
|
||||
outputTextColor: themeMode === 'dark' ? preferences?.palette?.outputTextColor?.dark || operatorSettingDefaults.palette.outputTextColor.dark :
|
||||
preferences?.palette?.outputTextColor?.light || operatorSettingDefaults.palette.outputTextColor.light,
|
||||
borderColor: themeMode === 'dark' ? preferences?.palette?.borderColor?.dark || operatorSettingDefaults.palette.borderColor.dark :
|
||||
preferences?.palette?.borderColor?.light || operatorSettingDefaults.palette.borderColor.light,
|
||||
})
|
||||
}catch(error){
|
||||
console.log(error);
|
||||
snackActions.error(error.message);
|
||||
return createTheme({
|
||||
transitions: {
|
||||
// So we have `transition: none;` everywhere
|
||||
create: () => 'none',
|
||||
},
|
||||
mode: themeMode,
|
||||
background: {
|
||||
contrast: themeMode === 'dark' ? preferences?.palette?.background?.light || operatorSettingDefaults.palette.background.light :
|
||||
preferences?.palette?.background?.dark || operatorSettingDefaults.palette.background.dark,
|
||||
default: themeMode === "dark" ? preferences?.palette?.background?.dark || operatorSettingDefaults.palette.background.dark :
|
||||
preferences?.palette?.background?.light || operatorSettingDefaults.palette.background.light,
|
||||
paper: themeMode === "dark" ? preferences?.palette?.paper?.dark || operatorSettingDefaults.palette.paper.dark :
|
||||
preferences?.palette?.paper?.light || operatorSettingDefaults.palette.paper.light,
|
||||
image: themeMode === "dark" ? preferences?.palette?.backgroundImage?.dark || operatorSettingDefaults.palette.backgroundImage.dark :
|
||||
preferences?.palette?.backgroundImage?.light || operatorSettingDefaults.palette.backgroundImage.light,
|
||||
palette: {
|
||||
contrastThreshold: 4.5,
|
||||
//tonalOffset: 0.5,
|
||||
primary: {
|
||||
main: themeMode === "dark" ? operatorSettingDefaults.palette.primary.dark :
|
||||
operatorSettingDefaults.palette.primary.light,
|
||||
},
|
||||
error: {
|
||||
main: themeMode === "dark" ? operatorSettingDefaults.palette.error.dark :
|
||||
operatorSettingDefaults.palette.error.light,
|
||||
},
|
||||
success: {
|
||||
main: themeMode === 'dark' ? operatorSettingDefaults.palette.success.dark :
|
||||
operatorSettingDefaults.palette.success.light,
|
||||
},
|
||||
secondary: {
|
||||
main: themeMode === 'dark' ? operatorSettingDefaults.palette.secondary.dark :
|
||||
operatorSettingDefaults.palette.secondary.light,
|
||||
},
|
||||
info: {
|
||||
main: themeMode === 'dark' ? operatorSettingDefaults.palette.info.dark :
|
||||
operatorSettingDefaults.palette.info.light,
|
||||
},
|
||||
warning: {
|
||||
main: themeMode === 'dark' ? operatorSettingDefaults.palette.warning.dark :
|
||||
operatorSettingDefaults.palette.warning.light,
|
||||
},
|
||||
mode: themeMode,
|
||||
background: {
|
||||
contrast: themeMode === 'dark' ?operatorSettingDefaults.palette.background.light :
|
||||
operatorSettingDefaults.palette.background.dark,
|
||||
default: themeMode === "dark" ? operatorSettingDefaults.palette.background.dark :
|
||||
operatorSettingDefaults.palette.background.light,
|
||||
paper: themeMode === "dark" ? operatorSettingDefaults.palette.paper.dark :
|
||||
operatorSettingDefaults.palette.paper.light,
|
||||
image: themeMode === "dark" ? operatorSettingDefaults.palette.backgroundImage.dark :
|
||||
operatorSettingDefaults.palette.backgroundImage.light,
|
||||
},
|
||||
text: {
|
||||
primary: themeMode === 'dark' ? operatorSettingDefaults.palette.text.dark :
|
||||
operatorSettingDefaults.palette.text.light,
|
||||
contrast: themeMode === 'dark' ? '#000' : '#fff',
|
||||
},
|
||||
graphGroupRGBA: themeMode === 'dark' ? 'rgba(57, 76, 93, 0.5)' : 'rgba(211, 215, 232, 0.5)',
|
||||
speedDialAction: themeMode === 'dark' ? '#495054' : '#ffffff',
|
||||
},
|
||||
text: {
|
||||
primary: themeMode === 'dark' ? preferences?.palette?.text?.dark || operatorSettingDefaults.palette.text.dark :
|
||||
preferences?.palette?.text?.light || operatorSettingDefaults.palette.text.light,
|
||||
contrast: themeMode === 'dark' ? '#000' : '#fff',
|
||||
folderColor: '#f1d592',
|
||||
tableHeader: themeMode === 'dark' ? operatorSettingDefaults.palette.tableHeader.dark :
|
||||
operatorSettingDefaults.palette.tableHeader.light,
|
||||
selectedCallbackColor: themeMode === 'dark' ? operatorSettingDefaults.palette.selectedCallbackColor.dark :
|
||||
operatorSettingDefaults.palette.selectedCallbackColor.light,
|
||||
selectedCallbackHierarchyColor: themeMode === 'dark' ? operatorSettingDefaults.palette.selectedCallbackHierarchyColor.dark :
|
||||
operatorSettingDefaults.palette.selectedCallbackHierarchyColor.light,
|
||||
tableHover: themeMode === 'dark' ? operatorSettingDefaults.palette.tableHover.dark :
|
||||
operatorSettingDefaults.palette.tableHover.light,
|
||||
navBarTextIconColor: themeMode === 'dark' ? operatorSettingDefaults.palette.navBarIcons.dark :
|
||||
operatorSettingDefaults.palette.navBarIcons.light,
|
||||
navBarTextColor: themeMode === 'dark' ? operatorSettingDefaults.palette.navBarText.dark :
|
||||
operatorSettingDefaults.palette.navBarText.light,
|
||||
pageHeader: {
|
||||
main: themeMode === 'dark' ? operatorSettingDefaults.palette.pageHeader.dark :
|
||||
operatorSettingDefaults.palette.pageHeader.light,
|
||||
},
|
||||
graphGroupRGBA: themeMode === 'dark' ? 'rgba(57, 76, 93, 0.5)' : 'rgba(211, 215, 232, 0.5)',
|
||||
speedDialAction: themeMode === 'dark' ? '#495054' : '#ffffff',
|
||||
},
|
||||
folderColor: '#f1d592',
|
||||
tableHeader: themeMode === 'dark' ? preferences?.palette?.tableHeader?.dark || operatorSettingDefaults.palette.tableHeader.dark :
|
||||
preferences?.palette?.tableHeader?.light || operatorSettingDefaults.palette.tableHeader.light,
|
||||
selectedCallbackColor: themeMode === 'dark' ? preferences?.palette?.selectedCallbackColor?.dark || operatorSettingDefaults.palette.selectedCallbackColor.dark :
|
||||
preferences?.palette?.selectedCallbackColor?.light || operatorSettingDefaults.palette.selectedCallbackColor.light,
|
||||
selectedCallbackHierarchyColor: themeMode === 'dark' ? preferences?.palette?.selectedCallbackHierarchyColor?.dark || operatorSettingDefaults.palette.selectedCallbackHierarchyColor.dark :
|
||||
preferences?.palette?.selectedCallbackHierarchyColor?.light || operatorSettingDefaults.palette.selectedCallbackHierarchyColor.light,
|
||||
tableHover: themeMode === 'dark' ? preferences?.palette?.tableHover?.dark || operatorSettingDefaults.palette.tableHover.dark :
|
||||
preferences?.palette?.tableHover?.light || operatorSettingDefaults.palette.tableHover.light,
|
||||
navBarTextIconColor: themeMode === 'dark' ? preferences?.palette?.navBarIcons?.dark || operatorSettingDefaults.palette.navBarIcons.dark :
|
||||
preferences?.palette?.navBarIcons?.light || operatorSettingDefaults.palette.navBarIcons.light,
|
||||
navBarTextColor: themeMode === 'dark' ? preferences?.palette?.navBarText?.dark || operatorSettingDefaults.palette.navBarText.dark :
|
||||
preferences?.palette?.navBarText?.light || operatorSettingDefaults.palette.navBarText.light,
|
||||
pageHeader: {
|
||||
main: themeMode === 'dark' ? preferences?.palette?.pageHeader?.dark || operatorSettingDefaults.palette.pageHeader.dark :
|
||||
preferences?.palette?.pageHeader?.light || operatorSettingDefaults.palette.pageHeader.light,
|
||||
},
|
||||
pageHeaderText: {
|
||||
main: 'white',
|
||||
},
|
||||
topAppBarColor: themeMode === 'dark' ? preferences?.palette?.navBarColor?.dark || operatorSettingDefaults.palette.navBarColor.dark :
|
||||
preferences?.palette?.navBarColor?.light || operatorSettingDefaults.palette.navBarColor.light,
|
||||
typography: {
|
||||
fontSize: preferences?.fontSize,
|
||||
fontFamily: preferences?.fontFamily
|
||||
},
|
||||
}),
|
||||
[themeMode, loadingPreference, preferences.fontSize, preferences.fontFamily, preferences.palette]
|
||||
pageHeaderText: {
|
||||
main: 'white',
|
||||
},
|
||||
topAppBarColor: themeMode === 'dark' ? operatorSettingDefaults.palette.navBarColor.dark :
|
||||
operatorSettingDefaults.palette.navBarColor.light,
|
||||
topAppBarBottomColor: themeMode === 'dark' ? operatorSettingDefaults.palette.navBarBottomColor.dark :
|
||||
operatorSettingDefaults.palette.navBarBottomColor.light,
|
||||
emptyFolderColor: themeMode === 'dark' ? operatorSettingDefaults.palette.emptyFolderColor.dark :
|
||||
operatorSettingDefaults.palette.emptyFolderColor.light,
|
||||
outputBackgroundColor: themeMode === 'dark' ? operatorSettingDefaults.palette.outputBackgroundColor.dark :
|
||||
operatorSettingDefaults.palette.outputBackgroundColor.light,
|
||||
outputTextColor: themeMode === 'dark' ? operatorSettingDefaults.palette.outputTextColor.dark :
|
||||
operatorSettingDefaults.palette.outputTextColor.light,
|
||||
borderColor: themeMode === 'dark' ? operatorSettingDefaults.palette.borderColor.dark :
|
||||
operatorSettingDefaults.palette.borderColor.light,
|
||||
typography: {
|
||||
fontSize: 12,//operatorSettingDefaults?.fontSize,
|
||||
fontFamily: operatorSettingDefaults?.fontFamily
|
||||
},
|
||||
})
|
||||
}
|
||||
},[themeMode, loadingPreference, preferences.fontSize, preferences.fontFamily, preferences.palette]
|
||||
);
|
||||
const mountedRef = React.useRef(true);
|
||||
const [openRefreshDialog, setOpenRefreshDialog] = React.useState(false);
|
||||
const [getUserPreferences] = useLazyQuery(userSettingsQuery, {
|
||||
fetchPolicy: "no-cache",
|
||||
onCompleted: (data) => {
|
||||
//console.log("got preferences", data.getOperatorPreferences.preferences)
|
||||
if(data.getOperatorPreferences.status === "success"){
|
||||
@@ -163,7 +289,7 @@ export function App(props) {
|
||||
let millisecondsLeft = JWTTimeLeft();
|
||||
// if we have 30min left of our token, prompt the user to extend. 30 min is 1,800,000 milliseconds
|
||||
//console.log("jwt time left: ", millisecondsLeft)
|
||||
if(millisecondsLeft <= 1800000 && !openRefreshDialog){
|
||||
if(millisecondsLeft <= 1800000 && !openRefreshDialog && me.loggedIn){
|
||||
if(isJWTValid()){
|
||||
setOpenRefreshDialog(true);
|
||||
}else{
|
||||
@@ -191,7 +317,9 @@ export function App(props) {
|
||||
)
|
||||
}
|
||||
const background = theme.palette.background.image !== null ? {
|
||||
background: "linear-gradient(" + theme.palette.background.default + "99" + "," + theme.palette.background.default + "99" + ")," + theme.palette.background.image,
|
||||
backgroundImage: "linear-gradient(" + theme.palette.background.default + "99" + "," + theme.palette.background.default + "99" + ")," + theme.palette.background.image ,
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundSize: "100% 100%"
|
||||
} : {
|
||||
backgroundColor: theme.palette.background.default
|
||||
};
|
||||
@@ -200,18 +328,19 @@ export function App(props) {
|
||||
<ThemeProvider theme={theme}>
|
||||
<GlobalStyles theme={theme} />
|
||||
<CssBaseline />
|
||||
<Tooltip id={"my-tooltip"} style={{zIndex: 100000, wordBreak: "break-word", maxWidth: "80%", whiteSpace: "pre-wrap"}}/>
|
||||
<ToastContainer limit={2} autoClose={3000}
|
||||
theme={themeMode}
|
||||
hideProgressBar={true}
|
||||
newestOnTop={true}
|
||||
stacked={false}
|
||||
style={{maxWidth: "100%", minWidth: "40%", width: "40%", display: "flex", flexWrap: "wrap",
|
||||
wordBreak: "break-all", flexDirection: "column", justifyContent: "center",}}
|
||||
pauseOnFocusLoss={false} />
|
||||
<MeContext.Provider value={me}>
|
||||
<Tooltip id={"my-tooltip"} style={{zIndex: 100000, wordBreak: "break-word", maxWidth: "80%", whiteSpace: "pre-wrap"}}/>
|
||||
<ToastContainer limit={2} autoClose={3000}
|
||||
theme={themeMode}
|
||||
hideProgressBar={true}
|
||||
newestOnTop={true}
|
||||
stacked={false}
|
||||
style={{maxWidth: "100%", minWidth: "40%", display: "flex", flexWrap: "wrap",
|
||||
wordBreak: "break-all", flexDirection: "column", justifyContent: "center",}}
|
||||
pauseOnFocusLoss={false}
|
||||
/>
|
||||
<div style={{ maxHeight: '100%', height: '100%', display: 'flex', flexDirection: 'row', maxWidth: "100%", width:"100%",
|
||||
...background}}>
|
||||
|
||||
{openRefreshDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openRefreshDialog}
|
||||
onClose={()=>{setOpenRefreshDialog(false);}}
|
||||
@@ -219,16 +348,17 @@ export function App(props) {
|
||||
onClose={()=>{setOpenRefreshDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
{me.loggedIn && me.user !== undefined && me.user !== null ? (
|
||||
<TopAppBarVertical me={me} theme={themeMode} toggleTheme={themeToggler} />
|
||||
) : null}
|
||||
{me.loggedIn && me.user !== undefined && me.user !== null &&
|
||||
<TopAppBarVertical me={me} toggleTheme={themeToggler} />
|
||||
}
|
||||
<div style={{
|
||||
maxHeight: '100%',
|
||||
height: '100%',
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: "100%"
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<div style={{height: "5px", width: "100%", background: me.loggedIn ? `linear-gradient(25deg, ${theme.topAppBarColor}, ${theme.topAppBarBottomColor})` : ""}}/>
|
||||
{me.loggedIn && me?.user?.current_operation_banner_text !== "" &&
|
||||
<Typography style={{
|
||||
backgroundColor: me?.user?.current_operation_banner_color,
|
||||
@@ -257,9 +387,9 @@ export function App(props) {
|
||||
<div style={{
|
||||
margin: '0px 0px 0px 0px',
|
||||
flexGrow: 1,
|
||||
overflowY: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: 'column',
|
||||
height: "100%",
|
||||
overflow:"hidden"
|
||||
}}>
|
||||
<Routes>
|
||||
<Route path='/new/login' element={<LoginForm me={me}/>}/>
|
||||
@@ -309,18 +439,15 @@ export function App(props) {
|
||||
element={<LoggedInRoute me={me}><MitreAttack me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/tagtypes'
|
||||
element={<LoggedInRoute me={me}><Tags me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/consuming_services'
|
||||
element={<LoggedInRoute me={me}><ConsumingServices
|
||||
me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/eventing'
|
||||
element={<LoggedInRoute me={me}><Eventing me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/jupyter' element={<LoggedInRoute me={me}><Jupyter/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/hasura' element={<LoggedInRoute me={me}><Hasura/></LoggedInRoute>}/>
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</MeContext.Provider>
|
||||
</ThemeProvider>
|
||||
</StyledEngineProvider>
|
||||
);
|
||||
|
||||
@@ -17,6 +17,7 @@ const subscribe_payloads = gql`
|
||||
level
|
||||
resolved
|
||||
source
|
||||
warning
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -33,7 +34,10 @@ export function EventFeedNotifications(props) {
|
||||
},
|
||||
onData: ({data}) => {
|
||||
if (data.data.operationeventlog_stream.length > 0){
|
||||
if(data.data.operationeventlog_stream[0].level === "api" || data.data.operationeventlog_stream[0].level === "debug" ){
|
||||
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){
|
||||
@@ -41,14 +45,14 @@ export function EventFeedNotifications(props) {
|
||||
}
|
||||
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].level === "warning") {
|
||||
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].level === "warning") {
|
||||
if (data.data.operationeventlog_stream[0].warning) {
|
||||
snackActions.warning(data.data.operationeventlog_stream[0].message, {autoClose: 3000});
|
||||
} else {
|
||||
snackActions.info(
|
||||
|
||||
@@ -1,13 +1,53 @@
|
||||
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';
|
||||
|
||||
export const MythicAgentSVGIcon = ({payload_type, style}) => {
|
||||
export const MythicAgentSVGIcon = ({payload_type, style, is_p2p}) => {
|
||||
const theme = useTheme();
|
||||
if(payload_type === ""){
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<MythicStyledTooltip title={payload_type}>
|
||||
<MythicStyledTooltip title={payload_type} >
|
||||
<img src={"/static/" + payload_type + "_" + theme.palette.mode + ".svg"}
|
||||
style={{...style}}/>
|
||||
style={{...style}} />
|
||||
{is_p2p === false &&
|
||||
<WifiIcon style={{width: "20px", height: "20px", marginLeft: "-20px", marginBottom: "5px",
|
||||
borderRadius: "10px", border: "1px solid",
|
||||
backgroundColor: theme.palette.primary.main}}/>
|
||||
}
|
||||
{is_p2p === true &&
|
||||
<FontAwesomeIcon icon={faLink} style={{width: "20px", height: "20px", marginLeft: "-20px", marginBottom: "5px",
|
||||
borderRadius: "10px", border: "1px solid",
|
||||
backgroundColor: theme.palette.primary.main}} />
|
||||
}
|
||||
</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}>
|
||||
<img src={"/static/" + payload_type + "_" + theme.palette.mode + ".svg"} style={{height: "20px", margin: 'auto', ...extraStyles}}/>
|
||||
{is_p2p === false &&
|
||||
<WifiIcon style={{width: "10px", height: "10px", marginLeft: "-5px", marginBottom: "0px",
|
||||
borderRadius: "10px", border: "1px solid",
|
||||
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>
|
||||
|
||||
|
||||
)
|
||||
}
|
||||
@@ -18,10 +18,29 @@ import "ace-builds/src-noconflict/theme-github";
|
||||
import "ace-builds/src-noconflict/theme-monokai";
|
||||
import "ace-builds/src-noconflict/ext-searchbox";
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import WrapTextIcon from '@mui/icons-material/WrapText';
|
||||
import {IconButton} from '@mui/material';
|
||||
import {MythicStyledTooltip} from "./MythicStyledTooltip";
|
||||
import Draggable from 'react-draggable';
|
||||
import {MythicDraggableDialogTitle} from "./MythicDraggableDialogTitle";
|
||||
|
||||
export function MythicDialog(props) {
|
||||
const descriptionElementRef = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
const [draggedState, setDraggedState] = React.useState({
|
||||
style: {},
|
||||
paperStyle: {
|
||||
margin: "0",
|
||||
height: "fit-content",
|
||||
width: "stretch"
|
||||
},
|
||||
containerStyle: {
|
||||
|
||||
},
|
||||
hideBackdrop: false,
|
||||
modified: false
|
||||
});
|
||||
const nodeRef = React.useRef(null);
|
||||
const descriptionElementRef = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
if (props.open) {
|
||||
const { current: descriptionElement } = descriptionElementRef;
|
||||
if (descriptionElement !== null) {
|
||||
@@ -30,35 +49,97 @@ export function MythicDialog(props) {
|
||||
}
|
||||
}, [props.open]);
|
||||
const dialogOnClick = (e) => {
|
||||
e.stopPropagation();
|
||||
//e.preventDefault();
|
||||
//e.stopPropagation();
|
||||
if(e.target.classList.length > 0 && e.target.classList.contains("MuiDialog-container")){
|
||||
props.onClose();
|
||||
props.onClose();
|
||||
}
|
||||
}
|
||||
const dialogOnContextMenu = (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
}
|
||||
const handleOnClose = (event, reason) => {
|
||||
if(reason === "backdropClick" && draggedState.hideBackdrop){
|
||||
return;
|
||||
}
|
||||
props.onClose();
|
||||
}
|
||||
const onStart = (e) => {
|
||||
if(e){
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
if(!draggedState.modified){
|
||||
setDraggedState({
|
||||
style: {
|
||||
height: e.target.offsetParent.offsetHeight + "px",
|
||||
width: e.target.offsetParent.offsetWidth + "px",
|
||||
margin: "auto",
|
||||
overflowY: "auto",
|
||||
},
|
||||
paperStyle: {
|
||||
//height: e.target.offsetParent.offsetHeight + "px",
|
||||
width: e.target.offsetParent.offsetWidth + "px",
|
||||
margin: 0,
|
||||
overflowY: "auto",
|
||||
},
|
||||
containerStyle: {
|
||||
height: "fit-content",
|
||||
overflowY: "auto"
|
||||
},
|
||||
hideBackdrop: true,
|
||||
modified: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
const onStop = (e) => {
|
||||
if(e){
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Dialog
|
||||
open={props.open}
|
||||
onClose={props.onClose}
|
||||
scroll="paper"
|
||||
maxWidth={props.maxWidth}
|
||||
fullWidth={props.fullWidth}
|
||||
style={props.style}
|
||||
aria-labelledby="scroll-dialog-title"
|
||||
aria-describedby="scroll-dialog-description"
|
||||
onMouseDown={dialogOnClick}
|
||||
onContextMenu={dialogOnContextMenu}
|
||||
<Draggable
|
||||
nodeRef={nodeRef}
|
||||
handle="#mythic-draggable-title"
|
||||
cancel={'[class*="MuiDialogContent-root"]'}
|
||||
onStart={onStart}
|
||||
onStop={onStop}
|
||||
>
|
||||
{props.innerDialog}
|
||||
</Dialog>
|
||||
<Dialog
|
||||
ref={nodeRef}
|
||||
open={props.open}
|
||||
onClick={dialogOnClick}
|
||||
onClose={handleOnClose}
|
||||
scroll="paper"
|
||||
maxWidth={props.maxWidth}
|
||||
fullWidth={true}
|
||||
style={{...props.style, ...draggedState.style}}
|
||||
disableEnforceFocus={true}
|
||||
disablePortal={false}
|
||||
hideBackdrop={draggedState.hideBackdrop}
|
||||
aria-labelledby="scroll-dialog-title"
|
||||
aria-describedby="scroll-dialog-description"
|
||||
sx={{
|
||||
[".MuiPaper-root"]: {
|
||||
...draggedState.paperStyle
|
||||
},
|
||||
[".MuiDialog-container"]: {
|
||||
...draggedState.containerStyle
|
||||
}
|
||||
}}
|
||||
onMouseDown={dialogOnClick}
|
||||
onContextMenu={dialogOnContextMenu}
|
||||
>
|
||||
{props.innerDialog}
|
||||
</Dialog>
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
export function MythicModifyStringDialog(props) {
|
||||
const [comment, setComment] = React.useState("");
|
||||
const [wrap, setWrap] = React.useState(props.wrap ? props.wrap : false);
|
||||
const theme = useTheme();
|
||||
const onCommitSubmit = () => {
|
||||
props.onSubmit(comment);
|
||||
@@ -80,35 +161,48 @@ export function MythicModifyStringDialog(props) {
|
||||
}, [props.value]);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">{props.title}</DialogTitle>
|
||||
<DialogContent dividers={true} style={{height: "100%", margin: 0, padding: 0}}>
|
||||
{props.title !== "" &&
|
||||
<MythicDraggableDialogTitle>{props.title}
|
||||
<MythicStyledTooltip title={wrap ? "Toggle off word wrap" : "Toggl on word wrap"}
|
||||
tooltipStyle={{float: "right"}}>
|
||||
<IconButton onClick={() => {setWrap(!wrap)}}>
|
||||
<WrapTextIcon color={wrap ? "success" : "secondary"} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</MythicDraggableDialogTitle>
|
||||
}
|
||||
<DialogContent dividers={true} style={{ margin: 0, padding: 0}}>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
|
||||
width="100%"
|
||||
height="100%"
|
||||
fontSize={14}
|
||||
showPrintMargin={false}
|
||||
wrapEnabled={props.wrap ? props.wrap : false}
|
||||
minLines={props.maxRows ? props.maxRows : 10}
|
||||
maxLines={props.maxRows ? props.maxRows : 10}
|
||||
wrapEnabled={wrap}
|
||||
value={comment}
|
||||
focus={true}
|
||||
onChange={onChange}
|
||||
setOptions={{
|
||||
useWorker: false
|
||||
tabSize: 4,
|
||||
useWorker: false,
|
||||
showInvisibles: false,
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
Close
|
||||
</Button>
|
||||
{props.onSubmit &&
|
||||
<Button onClick={onCommitSubmit} variant="contained" color="success">
|
||||
{props.onSubmitText ? props.onSubmitText : "Submit"}
|
||||
</Button>
|
||||
}
|
||||
</DialogActions>
|
||||
{ (props.onClose || props.onSubmit) &&
|
||||
<DialogActions>
|
||||
{props.onClose &&
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
Close
|
||||
</Button>
|
||||
}
|
||||
{props.onSubmit &&
|
||||
<Button onClick={onCommitSubmit} variant="contained" color="success">
|
||||
{props.onSubmitText ? props.onSubmitText : "Submit"}
|
||||
</Button>
|
||||
}
|
||||
</DialogActions>
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -180,7 +274,7 @@ export function MythicViewJSONAsTableDialog(props) {
|
||||
}, [props.value, props.leftColumn, props.rightColumn]);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title" style={{wordBreak: "break-all", maxWidth: "100%"}}>{props.title}</DialogTitle>
|
||||
<MythicDraggableDialogTitle style={{wordBreak: "break-all", maxWidth: "100%"}}>{props.title}</MythicDraggableDialogTitle>
|
||||
|
||||
<TableContainer className="mythicElement" style={{paddingLeft: "10px"}}>
|
||||
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
|
||||
|
||||
export const MythicDraggableDialogTitle = ({children}) => {
|
||||
return (
|
||||
<DialogTitle id="mythic-draggable-title" style={{ cursor: 'move', width: "100%" }}>
|
||||
{children}
|
||||
</DialogTitle>
|
||||
)
|
||||
}
|
||||
@@ -239,7 +239,7 @@ const VirtualTreeRow = React.memo(({
|
||||
height: "15px",
|
||||
marginLeft: '3px',
|
||||
marginRight: '5px',
|
||||
color: item?.metadata?.has_children ? theme.folderColor : theme.palette.text.secondary,
|
||||
color: item?.has_children || item.success ? theme.folderColor : theme.emptyFolderColor,
|
||||
}}
|
||||
size={"lg"}
|
||||
onClick={handleOnClickButton} />
|
||||
@@ -250,14 +250,14 @@ const VirtualTreeRow = React.memo(({
|
||||
height: "15px",
|
||||
marginLeft: '3px',
|
||||
marginRight: '5px',
|
||||
color: item?.metadata?.has_children ? theme.folderColor : theme.palette.text.secondary, }}
|
||||
color: item?.has_children || item.success ? theme.folderColor : theme.emptyFolderColor, }}
|
||||
size={"lg"}
|
||||
icon={faFolder} onClick={handleOnClickButton} />
|
||||
)}
|
||||
<Typography
|
||||
style={{
|
||||
color:
|
||||
item?.metadata?.has_children ||
|
||||
item?.has_children ||
|
||||
item.success !== null
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary,
|
||||
@@ -439,7 +439,7 @@ const FileBrowserVirtualTreePreMemo = ({
|
||||
{(AutoSizerProps) => (
|
||||
<List
|
||||
itemData={flattenedNodes}
|
||||
direction="vertical"
|
||||
layout="vertical"
|
||||
height={AutoSizerProps.height}
|
||||
width={AutoSizerProps.width}
|
||||
itemCount={flattenedNodes.length}
|
||||
|
||||
@@ -14,6 +14,7 @@ export const getfileInformationQuery = gql`
|
||||
query getFileInformation($file_id: String!){
|
||||
filemeta(where: {agent_file_id: {_eq: $file_id}}){
|
||||
filename_text
|
||||
agent_file_id
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -33,31 +34,55 @@ export const MythicFileContext = ({agent_file_id, display_link, filename, extraS
|
||||
setOpenPreviewMediaDialog(true);
|
||||
}
|
||||
const getFileInformationSuccess = (data) => {
|
||||
setFileData( {...fileData, filename: b64DecodeUnicode(data.filemeta[0].filename_text)});
|
||||
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)});
|
||||
}
|
||||
}
|
||||
const getFileInformationError = (data) => {
|
||||
snackActions.error("Failed to fetch instance data: " + 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}})
|
||||
.then(({data}) => getFileInformationSuccess(data)).catch(({data}) => getFileInformationError(data));
|
||||
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 Media"} tooltipStyle={extraStyles ? extraStyles : {}}>
|
||||
<FontAwesomeIcon icon={faPhotoVideo}
|
||||
style={{height: "25px", bottom: "5px", position: "relative", cursor: "pointer", display: "inline-block"}}
|
||||
style={{height: "20px", position: "relative", cursor: "pointer", display: "inline-block"}}
|
||||
onClick={onPreviewMedia} />
|
||||
</MythicStyledTooltip>
|
||||
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" href={"/direct/download/" + fileData.agent_file_id}>
|
||||
@@ -65,11 +90,11 @@ export const MythicFileContext = ({agent_file_id, display_link, filename, extraS
|
||||
</Link>
|
||||
{openPreviewMediaDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openPreviewMediaDialog}
|
||||
onClose={(e)=>{setOpenPreviewMediaDialog(false);}}
|
||||
onClose={onClose}
|
||||
innerDialog={<PreviewFileMediaDialog
|
||||
agent_file_id={fileData.agent_file_id}
|
||||
filename={fileData.filename}
|
||||
onClose={(e)=>{setOpenPreviewMediaDialog(false);}} />}
|
||||
onClose={onClose} />}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
export const MythicPageBody = ({children}) => {
|
||||
return (
|
||||
<div style={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
margin: "0.5rem",
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {Typography} from "@mui/material";
|
||||
|
||||
export const MythicPageHeader = ({children, title, style={}, headerVariant="h5"}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Paper elevation={2}
|
||||
variant={"elevation"}
|
||||
style={{
|
||||
backgroundColor: theme.pageHeader.main,
|
||||
color: theme.pageHeaderText.main,
|
||||
border: `1px solid ${theme.borderColor}`,
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: "0.5rem",
|
||||
padding: "0.5rem",
|
||||
borderRadius: "5px",
|
||||
...style
|
||||
}}
|
||||
>
|
||||
<Typography variant={headerVariant} style={{textAlign: "left", display: "inline-block", marginLeft: "1rem", width: "100%"}}>
|
||||
{title}
|
||||
</Typography>
|
||||
<div style={{display: "flex", marginRight: "1rem", alignItems: "center"}}>
|
||||
{children}
|
||||
</div>
|
||||
</Paper>
|
||||
)
|
||||
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {Dropdown, DropdownMenuItem, DropdownNestedMenuItem} from "../MythicNeste
|
||||
const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
const [openContextMenu, setOpenContextMenu] = React.useState(false);
|
||||
const rowClassName = data.gridUUID + "row" + rowIndex;
|
||||
const rowHighlight = rowIndex % 2 === 1 ? 'MythicResizableGridRowHighlight' : '';
|
||||
const [contextMenuOptions, setContextMenuOptions] = React.useState(data?.rowContextMenuOptions || []);
|
||||
const dropdownAnchorRef = React.useRef(null);
|
||||
const item = data.items[rowIndex][columnIndex];
|
||||
@@ -21,6 +22,8 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
[data, rowIndex]
|
||||
);
|
||||
const handleClick = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if(data.onRowClick){
|
||||
data.onRowClick({event, rowDataStatic: data.items[rowIndex][columnIndex]?.props?.rowData})
|
||||
}
|
||||
@@ -43,6 +46,8 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
}
|
||||
}
|
||||
const handleMenuItemClick = (event, clickOption) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
clickOption({event, columnIndex, rowIndex, data: data.items[rowIndex][columnIndex]?.props?.rowData || {}});
|
||||
setOpenContextMenu(false);
|
||||
};
|
||||
@@ -54,7 +59,8 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
const handleContextClick = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
if(item.disableFilterMenu){
|
||||
event.stopPropagation();
|
||||
if(item?.disableFilterMenu){
|
||||
return;
|
||||
}
|
||||
contextMenuLocationRef.current.x = event.clientX;
|
||||
@@ -77,7 +83,7 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
);
|
||||
return (
|
||||
<div style={{...style, ...cellStyle, ...rowStyle}}
|
||||
className={`${classes.cell} ${rowClassName} ${selectedClass}`}
|
||||
className={`${classes.cell} ${rowClassName} ${rowHighlight} ${selectedClass}`}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
@@ -89,14 +95,14 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
{item}
|
||||
</div>
|
||||
<ContextMenu dropdownAnchorRef={dropdownAnchorRef} contextMenuOptions={contextMenuOptions}
|
||||
disableFilterMenu={item.disableFilterMenu} openContextMenu={openContextMenu}
|
||||
disableFilterMenu={item?.disableFilterMenu} openContextMenu={openContextMenu}
|
||||
contextMenuLocationRef={contextMenuLocationRef}
|
||||
setOpenContextMenu={setOpenContextMenu} handleMenuItemClick={handleMenuItemClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const ContextMenu = ({openContextMenu, dropdownAnchorRef, contextMenuOptions, setOpenContextMenu, handleMenuItemClick, contextMenuLocationRef}) => {
|
||||
export const ContextMenu = ({openContextMenu, dropdownAnchorRef, contextMenuOptions, setOpenContextMenu, handleMenuItemClick, contextMenuLocationRef}) => {
|
||||
const handleClose = (event) => {
|
||||
//if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
|
||||
// return;
|
||||
|
||||
+4
-2
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
||||
import {classes} from './styles';
|
||||
import Draggable from 'react-draggable';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import DragHandleIcon from '@mui/icons-material/DragHandle';
|
||||
|
||||
const DraggableHandles = React.forwardRef(({ height, rowHeight, width, minColumnWidth, columnWidths, onStop }, ref) => {
|
||||
const [isDragging, setIsDragging] = useState(-1);
|
||||
@@ -40,13 +41,14 @@ const DraggableHandles = React.forwardRef(({ height, rowHeight, width, minColumn
|
||||
setIsDragging(-1);
|
||||
onStop(data.x, i);
|
||||
}}>
|
||||
<MoreVertIcon
|
||||
<DragHandleIcon
|
||||
ref={nodeRef}
|
||||
className={isDragging === i ? classes.draggableHandlesClickAreaSelected : classes.draggableHandlesClickArea}
|
||||
style={{
|
||||
left: leftOffset + columnWidths[i] - 1 - 7,
|
||||
rotate: isDragging === i ? "0deg" : "90deg",
|
||||
}}>
|
||||
</MoreVertIcon>
|
||||
</DragHandleIcon>
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -5,13 +5,7 @@ import {classes} from './styles';
|
||||
import React from 'react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faFilter} from '@fortawesome/free-solid-svg-icons';
|
||||
import Grow from '@mui/material/Grow';
|
||||
import Popper from '@mui/material/Popper';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import MenuList from '@mui/material/MenuList';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import {ContextMenu} from "./Cell";
|
||||
|
||||
const HeaderCell = ({
|
||||
onClick = () => {},
|
||||
@@ -23,9 +17,9 @@ const HeaderCell = ({
|
||||
VariableSizeGridProps: { style, rowIndex, columnIndex, data },
|
||||
}) => {
|
||||
const dropdownAnchorRef = React.useRef(null);
|
||||
const theme = useTheme();
|
||||
const item = data.items[rowIndex][columnIndex];
|
||||
const isFiltered = item?.filtered || false;
|
||||
const contextMenuLocationRef = React.useRef({x: 0, y: 0});
|
||||
const handleClick = useCallback(
|
||||
(e) => {
|
||||
onClick(e, columnIndex);
|
||||
@@ -47,22 +41,20 @@ const HeaderCell = ({
|
||||
return;
|
||||
}
|
||||
if(contextMenuOptions && contextMenuOptions.length > 0){
|
||||
|
||||
contextMenuLocationRef.current.x = event.clientX;
|
||||
contextMenuLocationRef.current.y = event.clientY;
|
||||
setOpenContextMenu(true);
|
||||
}
|
||||
},
|
||||
[contextMenuOptions, columnIndex] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
);
|
||||
const handleMenuItemClick = (event, index) => {
|
||||
contextMenuOptions[index].click({event, columnIndex});
|
||||
const handleMenuItemClick = (event, clickOption) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
clickOption({event, columnIndex, rowIndex, data: data.items[rowIndex][columnIndex]?.props?.rowData || {}});
|
||||
setOpenContextMenu(false);
|
||||
};
|
||||
const handleClose = (event) => {
|
||||
if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
setOpenContextMenu(false);
|
||||
};
|
||||
|
||||
const handleClicks = useSingleAndDoubleClick(handleClick, handleDoubleClick);
|
||||
|
||||
return (
|
||||
@@ -73,31 +65,11 @@ const HeaderCell = ({
|
||||
</Typography>
|
||||
{isFiltered && <FontAwesomeIcon icon={faFilter} />}
|
||||
{sortIndicatorIndex === columnIndex && (sortDirection === 'ASC' ? <div>↑</div> : <div>↓</div>)}
|
||||
<Popper open={openContextMenu} anchorEl={dropdownAnchorRef.current} role={undefined} transition 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}}>
|
||||
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
|
||||
<MenuList id="split-button-menu" >
|
||||
{contextMenuOptions.map((option, index) => (
|
||||
<MenuItem
|
||||
key={option.name + index}
|
||||
onClick={(event) => handleMenuItemClick(event, index)}
|
||||
>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
<ContextMenu dropdownAnchorRef={dropdownAnchorRef} contextMenuOptions={contextMenuOptions}
|
||||
disableFilterMenu={item?.disableFilterMenu} openContextMenu={openContextMenu}
|
||||
contextMenuLocationRef={contextMenuLocationRef}
|
||||
setOpenContextMenu={setOpenContextMenu} handleMenuItemClick={handleMenuItemClick}
|
||||
/>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
|
||||
+18
-7
@@ -11,7 +11,7 @@ import {GetMythicSetting, useSetMythicSetting} from "../MythicSavedUserSetting";
|
||||
|
||||
const HeaderCellContext = createContext({});
|
||||
|
||||
const MIN_COLUMN_WIDTH = 20;
|
||||
const MIN_COLUMN_WIDTH = 100;
|
||||
const MIN_FLEX_COLUMN_WIDTH = 150;
|
||||
|
||||
|
||||
@@ -155,6 +155,9 @@ const ResizableGridWrapper = ({
|
||||
|
||||
useEffect(() => {
|
||||
gridRef.current.resetAfterColumnIndex(0, true);
|
||||
if(name !== undefined){
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: columnWidths});
|
||||
}
|
||||
}, [columnWidths]);
|
||||
|
||||
/* Event Handlers */
|
||||
@@ -192,7 +195,10 @@ const ResizableGridWrapper = ({
|
||||
return itemRow[columnIndex]?.props?.cellData.length;
|
||||
}
|
||||
if(columns[columnIndex].type === "size"){
|
||||
if(itemRow[columnIndex]?.props?.cellData){
|
||||
if(itemRow[columnIndex]?.props?.cellData !== undefined){
|
||||
if(typeof itemRow[columnIndex]?.props?.cellData === 'number'){
|
||||
return String(itemRow[columnIndex]?.props?.cellData)?.length;
|
||||
}
|
||||
return String(itemRow[columnIndex]?.props?.cellData?.plaintext)?.length;
|
||||
}
|
||||
return itemRow[columnIndex].length;
|
||||
@@ -249,12 +255,17 @@ const ResizableGridWrapper = ({
|
||||
}
|
||||
return Math.floor(columnWidth);
|
||||
});
|
||||
//console.log(updatedWidths, longestElementInColumn);
|
||||
setColumnWidths(updatedColumnWidths);
|
||||
if(name !== undefined){
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
|
||||
//console.log(updatedColumnWidths, columnWidths, longestElementInColumn);
|
||||
let updatedValues = false;
|
||||
for(let i = 0; i < updatedColumnWidths.length; i++){
|
||||
if(updatedColumnWidths[i] !== columnWidths[i]){
|
||||
updatedValues = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(updatedValues){
|
||||
setColumnWidths(() => updatedColumnWidths);
|
||||
}
|
||||
|
||||
}, [columnWidths]);
|
||||
|
||||
useEffect( () => {
|
||||
|
||||
@@ -14,6 +14,7 @@ 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 MythicStyledTableCell from "./MythicTableCell";
|
||||
|
||||
export function MythicSelectFromListDialog(props) {
|
||||
const [options, setOptions] = React.useState([]);
|
||||
@@ -89,20 +90,22 @@ 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>
|
||||
<div style={{height: "100%", display: "flex"}}>
|
||||
<TableContainer component={Paper} className="mythicElement" style={{flexGrow: 1, overflowY: "auto"}}>
|
||||
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
<TableBody style={{whiteSpace: "pre"}}>
|
||||
{options.map( (choice, i) => (
|
||||
<TableRow hover key={choice + i}>
|
||||
<MythicStyledTableCell style={{width: "5rem"}}>
|
||||
<Button onClick={() => handleSubmit(choice)} variant="contained" color="primary">Select</Button>
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>{choice}</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
Close
|
||||
|
||||
@@ -8,6 +8,7 @@ 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 Badge from '@mui/material/Badge';
|
||||
import {
|
||||
Draggable,
|
||||
} from "@hello-pangea/dnd";
|
||||
@@ -38,7 +39,7 @@ export function MythicTabPanel(props) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function a11yProps(index) {
|
||||
export function a11yProps(index) {
|
||||
return {
|
||||
id: `scrollable-auto-tab-${index}`,
|
||||
'aria-controls': `scrollable-auto-tabpanel-${index}`,
|
||||
@@ -60,16 +61,20 @@ export function MythicTabLabel(props) {
|
||||
getCallbackData,
|
||||
onDragTab,
|
||||
selectedIndex,
|
||||
highlight,
|
||||
newDataForTab,
|
||||
...other
|
||||
} = props;
|
||||
const onClick = (e) => {
|
||||
//e.stopPropagation();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onCloseTab({ tabID: tabInfo.tabID, index: index });
|
||||
};
|
||||
const [openContextMenu, setOpenContextMenu] = React.useState(false);
|
||||
const dropdownAnchorRef = React.useRef(null);
|
||||
const handleContextClick = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if(contextMenuOptions && contextMenuOptions.length > 0){
|
||||
setOpenContextMenu(true);
|
||||
}
|
||||
@@ -85,7 +90,7 @@ export function MythicTabLabel(props) {
|
||||
setOpenContextMenu(false);
|
||||
};
|
||||
return (
|
||||
<Draggable draggableId={`callbacks-tab-${label}`} index={index} disableInteractiveElementBlocking={true}>
|
||||
<Draggable draggableId={`callbacks-tab-${tabInfo.tabID}`} index={index} disableInteractiveElementBlocking={true}>
|
||||
{(provided, snapshot) => (
|
||||
<div ref={provided.innerRef}
|
||||
{...provided.draggableProps}>
|
||||
@@ -93,7 +98,9 @@ export function MythicTabLabel(props) {
|
||||
className={selectedIndex === index || snapshot.isDragging ? "selectedCallback" : "" }
|
||||
label={
|
||||
<span onContextMenu={handleContextClick} style={{}} ref={dropdownAnchorRef}>
|
||||
{label}
|
||||
<Badge color="success" variant="dot" invisible={!highlight} >
|
||||
{label}
|
||||
</Badge>
|
||||
<IconButton component='div' size='small' onClick={onClick} {...other}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {styled} from '@mui/material/styles';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
const MythicStyledTableCell = styled(TableCell, {})({
|
||||
padding: "0 0 0 10px !important",
|
||||
padding: "2px 0 2px 10px !important",
|
||||
});
|
||||
export default MythicStyledTableCell;
|
||||
@@ -26,6 +26,10 @@ import LocalOfferOutlinedIcon from '@mui/icons-material/LocalOfferOutlined';
|
||||
import {MythicStyledTooltip} from "./MythicStyledTooltip";
|
||||
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import MythicStyledTableCell from "./MythicTableCell";
|
||||
import {meState} from "../../cache";
|
||||
import { useReactiveVar } from '@apollo/client';
|
||||
import {MythicDraggableDialogTitle} from "./MythicDraggableDialogTitle";
|
||||
|
||||
const createNewTagMutationTemplate = ({target_object}) => {
|
||||
// target_object should be something like "task_id"
|
||||
@@ -96,6 +100,8 @@ query getSingleTag($tag_id: Int!){
|
||||
mythictree_id
|
||||
operation_id
|
||||
response_id
|
||||
callback_id
|
||||
payload_id
|
||||
task_id
|
||||
taskartifact_id
|
||||
tagtype {
|
||||
@@ -107,16 +113,16 @@ query getSingleTag($tag_id: Int!){
|
||||
}
|
||||
}
|
||||
`
|
||||
export const TagsDisplay = ({tags}) => {
|
||||
export const TagsDisplay = ({tags, expand}) => {
|
||||
return (
|
||||
tags?.map( tt => (
|
||||
<TagChipDisplay tag={tt} key={tt.id} />
|
||||
<TagChipDisplay tag={tt} key={tt.id} expand={expand} />
|
||||
))
|
||||
)
|
||||
}
|
||||
const TagChipDisplay = ({tag}) => {
|
||||
const TagChipDisplay = ({tag, expand}) => {
|
||||
const [openTagDisplay, setOpenTagDisplay] = React.useState(false);
|
||||
const [label, setLabel] = React.useState(tag.tagtype.name[0]);
|
||||
const [label, setLabel] = React.useState(expand ? tag.tagtype.name : tag.tagtype.name[0]);
|
||||
const onSelectTag = (event, tag) => {
|
||||
if(event){
|
||||
event.preventDefault();
|
||||
@@ -132,17 +138,21 @@ const TagChipDisplay = ({tag}) => {
|
||||
setOpenTagDisplay(false);
|
||||
}
|
||||
const onMouseOver = () => {
|
||||
setLabel(tag.tagtype.name);
|
||||
if(expand === undefined || !expand){
|
||||
setLabel(tag.tagtype.name);
|
||||
}
|
||||
}
|
||||
const onMouseOut = () => {
|
||||
setTimeout( () => {
|
||||
setLabel(tag.tagtype.name[0]);
|
||||
}, 10000); // wait 10s then go back to just a single letter
|
||||
if(expand === undefined || !expand){
|
||||
setTimeout( () => {
|
||||
setLabel(tag.tagtype.name[0]);
|
||||
}, 10000); // wait 10s then go back to just a single letter
|
||||
}
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Chip onMouseOver={onMouseOver} onMouseOut={onMouseOut} label={label} size="small" onClick={(e) => onSelectTag(e)}
|
||||
style={{float: "right", backgroundColor:tag.tagtype.color,}}
|
||||
style={{float: "right", backgroundColor:tag.tagtype.color, height: "15px"}}
|
||||
sx={{
|
||||
"& .MuiChip-label": {overflow: "visible"}
|
||||
}}
|
||||
@@ -233,6 +243,10 @@ function ViewTagDialog(props) {
|
||||
setObjectInfo({object_type: "task_id", object_id: data.tag_by_pk.task_id});
|
||||
}else if(data.tag_by_pk.taskartifact_id !== null){
|
||||
setObjectInfo({object_type: "taskartifact_id", object_id: data.tag_by_pk.taskartifact_id});
|
||||
}else if(data.tag_by_pk.payload_id !== null){
|
||||
setObjectInfo({object_type: "payload_id", object_id: data.tag_by_pk.payload_id});
|
||||
}else if(data.tag_by_pk.callback_id !== null){
|
||||
setObjectInfo({object_type: "callback_id", object_id: data.tag_by_pk.callback_id});
|
||||
}
|
||||
let newTag = {...data.tag_by_pk};
|
||||
let tagData = newTag;
|
||||
@@ -285,20 +299,20 @@ return (
|
||||
<TableCell>{selectedTag?.tagtype?.description || ""}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>Source</TableCell>
|
||||
<TableCell>
|
||||
<MythicStyledTableCell>Source</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
{selectedTag?.source ||""}
|
||||
</TableCell>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>Reference URL</TableCell>
|
||||
<TableCell>
|
||||
<MythicStyledTableCell>Reference URL</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
{selectedTag?.url === "" ? (
|
||||
"No reference link provided"
|
||||
) : (
|
||||
<Link href={selectedTag?.url || "#"} color="textPrimary" target="_blank" referrerPolicy='no'>{selectedTag?.url ? "click here" : "No reference link provided"}</Link>
|
||||
)}
|
||||
</TableCell>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Data</TableCell>
|
||||
@@ -309,31 +323,36 @@ return (
|
||||
<TableBody>
|
||||
{Object.keys(selectedTag.data).map( key => (
|
||||
<TableRow key={key} hover>
|
||||
<TableCell>{key}</TableCell>
|
||||
<MythicStyledTableCell>{key}</MythicStyledTableCell>
|
||||
{typeof selectedTag.data[key] === "string" ? (
|
||||
<TableCell style={{whiteSpace: "pre-wrap"}}>
|
||||
<MythicStyledTableCell style={{whiteSpace: "pre-wrap"}}>
|
||||
<StringTagDataEntry name={key} value={String(selectedTag.data[key])} />
|
||||
</TableCell>
|
||||
</MythicStyledTableCell>
|
||||
) : typeof selectedTag.data[key] === "object" ? (
|
||||
Array.isArray(selectedTag.data[key]) ? (
|
||||
<TableCell style={{whiteSpace: "pre-wrap"}}>{JSON.stringify(selectedTag.data[key], null, 2)}</TableCell>
|
||||
) : (
|
||||
<Table size={"small"} >
|
||||
{Object.keys(selectedTag.data[key]).map(key2 => (
|
||||
<TableRow key={key2} >
|
||||
<TableCell>{key2}</TableCell>
|
||||
<TableCell>
|
||||
<StringTagDataEntry name={key2} value={String(selectedTag.data[key][key2])} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</Table>
|
||||
<MythicStyledTableCell>
|
||||
<Table size={"small"} >
|
||||
<TableBody>
|
||||
{Object.keys(selectedTag.data[key]).map(key2 => (
|
||||
<TableRow key={key2} >
|
||||
<MythicStyledTableCell>{key2}</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<StringTagDataEntry name={key2} value={String(selectedTag.data[key][key2])} />
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
</Table>
|
||||
</MythicStyledTableCell>
|
||||
)
|
||||
) : typeof selectedTag.data[key] === "boolean" ? (
|
||||
<TableCell>{selectedTag.data[key] ? "True" : "False"}</TableCell>
|
||||
<MythicStyledTableCell>{selectedTag.data[key] ? "True" : "False"}</MythicStyledTableCell>
|
||||
) :
|
||||
(
|
||||
<TableCell>{String(selectedTag.data[key])}</TableCell>
|
||||
<MythicStyledTableCell>{String(selectedTag.data[key])}</MythicStyledTableCell>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -477,24 +496,21 @@ const onAcceptDelete = () => {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Edit Tags
|
||||
</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
{openNewDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="xl" open={openNewDialog}
|
||||
<MythicDraggableDialogTitle >Edit Tags </MythicDraggableDialogTitle>
|
||||
<DialogContent dividers={true} style={{width: "100%"}}>
|
||||
{openNewDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openNewDialog}
|
||||
onClose={()=>{setOpenNewDialog(false);}}
|
||||
innerDialog={<NewTagDialog me={props.me}
|
||||
target_object={props.target_object}
|
||||
target_object_id={props.target_object_id}
|
||||
onClose={()=>{setOpenNewDialog(false);}}
|
||||
onSubmit={handleNewTagCreate} />}
|
||||
/>) : null}
|
||||
<TableContainer className="mythicElement">
|
||||
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
/>}
|
||||
<Table size="small" style={{ width: "100%", overflow: "scroll"}}>
|
||||
<TableBody>
|
||||
<TableRow hover>
|
||||
<TableCell style={{width: "30%"}}>Select Existing Tag to Edit or Add New</TableCell>
|
||||
<TableCell style={{display: "inline-flex", flexDirection: "row-reverse"}}>
|
||||
<MythicStyledTableCell style={{width: "30%"}}>Select Existing Tag to Edit or Add New</MythicStyledTableCell>
|
||||
<MythicStyledTableCell style={{display: "inline-flex", flexDirection: "row-reverse"}}>
|
||||
<MythicStyledTooltip title={"Add New Tag"}>
|
||||
<IconButton variant='contained' color="success" style={{float: "right"}} onClick={() => setOpenNewDialog(true)} >
|
||||
<AddCircleOutlineIcon />
|
||||
@@ -519,28 +535,28 @@ return (
|
||||
{openDelete &&
|
||||
<MythicConfirmDialog onClose={() => {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete} open={openDelete}/>
|
||||
}
|
||||
</TableCell>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>Tag Description</TableCell>
|
||||
<TableCell>{selectedTag?.tagtype?.description || ""}</TableCell>
|
||||
<MythicStyledTableCell>Tag Description</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>{selectedTag?.tagtype?.description || ""}</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>Source</TableCell>
|
||||
<TableCell>
|
||||
<MythicStyledTableCell>Source</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<MythicTextField value={newSource} onChange={onChangeSource} name="Source of tag data" />
|
||||
</TableCell>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>External URL</TableCell>
|
||||
<TableCell>
|
||||
<MythicStyledTableCell>External URL</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<MythicTextField value={newURL} onChange={onChangeURL} name="External URL reference" />
|
||||
<Link href={newURL} color="textPrimary" target="_blank" referrerPolicy='no'>{newURL ? "click here" : ""}</Link>
|
||||
</TableCell>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>JSON Data</TableCell>
|
||||
<TableCell>
|
||||
<MythicStyledTableCell>JSON Data</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
|
||||
@@ -556,11 +572,10 @@ return (
|
||||
tabSize: 4,
|
||||
useWorker: false
|
||||
}}/>
|
||||
</TableCell>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
@@ -629,13 +644,12 @@ export function NewTagDialog(props) {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Add New Tag</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
<TableContainer className="mythicElement">
|
||||
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
<MythicDraggableDialogTitle >Add New Tag</MythicDraggableDialogTitle>
|
||||
<DialogContent dividers={true} style={{width: "100%"}}>
|
||||
<Table size="small" style={{ overflow: "scroll", width: "100%"}}>
|
||||
<TableBody>
|
||||
<TableRow hover>
|
||||
<TableCell style={{width: "20%"}}>
|
||||
<MythicStyledTableCell style={{width: "20%"}}>
|
||||
<Typography>
|
||||
Tag
|
||||
</Typography>
|
||||
@@ -647,8 +661,8 @@ export function NewTagDialog(props) {
|
||||
here
|
||||
</Link>
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<Select
|
||||
labelId="demo-dialog-select-label"
|
||||
id="demo-dialog-select"
|
||||
@@ -662,24 +676,24 @@ export function NewTagDialog(props) {
|
||||
</MenuItem>
|
||||
) )}
|
||||
</Select>
|
||||
</TableCell>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>Source</TableCell>
|
||||
<TableCell>
|
||||
<MythicStyledTableCell>Source</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<MythicTextField value={newSource} onChange={onChangeSource} name="Source of tag data" />
|
||||
</TableCell>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>External URL</TableCell>
|
||||
<TableCell>
|
||||
<MythicStyledTableCell>External URL</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<MythicTextField value={newURL} onChange={onChangeURL} name="External URL reference" />
|
||||
<Link href={newURL} color="textPrimary" target="_blank" referrerPolicy='no'>{newURL}</Link>
|
||||
</TableCell>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>JSON Data</TableCell>
|
||||
<TableCell>
|
||||
<MythicStyledTableCell>JSON Data</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
|
||||
@@ -695,11 +709,10 @@ export function NewTagDialog(props) {
|
||||
tabSize: 4,
|
||||
useWorker: false
|
||||
}}/>
|
||||
</TableCell>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
@@ -715,7 +728,8 @@ export function NewTagDialog(props) {
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
export const ViewEditTags = ({target_object, target_object_id, me}) => {
|
||||
export const ViewEditTags = ({target_object, target_object_id}) => {
|
||||
const me = useReactiveVar(meState);
|
||||
const [openTagDialog, setOpenTagDialog] = React.useState(false);
|
||||
const toggleTagDialog = (event, open) => {
|
||||
if(event){
|
||||
@@ -726,7 +740,10 @@ export const ViewEditTags = ({target_object, target_object_id, me}) => {
|
||||
}
|
||||
return(
|
||||
<React.Fragment>
|
||||
<IconButton onClick={(e) => toggleTagDialog(e, true)} size="small" style={{display: "inline-block", float: "right"}}><LocalOfferOutlinedIcon /></IconButton>
|
||||
<IconButton onClick={(e) => toggleTagDialog(e, true)} size="small"
|
||||
style={{display: "inline-block", float: "right", padding: "0px"}}>
|
||||
<LocalOfferOutlinedIcon />
|
||||
</IconButton>
|
||||
{openTagDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openTagDialog}
|
||||
onClose={(e)=>{toggleTagDialog(e, false)}}
|
||||
|
||||
@@ -53,7 +53,7 @@ const MythicTextField = ({
|
||||
marginBottom = "5px",
|
||||
value,
|
||||
disabled = false,
|
||||
marginTop = "0px",
|
||||
marginTop = "5px",
|
||||
InputProps = {},
|
||||
inputLabelProps = {},
|
||||
multiline = false,
|
||||
@@ -121,7 +121,7 @@ const MythicTextField = ({
|
||||
style={{
|
||||
padding:0,
|
||||
marginBottom: marginBottom ? marginBottom : "5px",
|
||||
marginTop: marginTop ? marginTop: "0px",
|
||||
marginTop: marginTop ? marginTop: "5px",
|
||||
display: inline ? "inline-block": "",
|
||||
}}
|
||||
classes={{
|
||||
|
||||
@@ -78,6 +78,8 @@ import {
|
||||
} from "@hello-pangea/dnd";
|
||||
import {reorder} from "./MythicComponents/MythicDraggableList";
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import LogoutIcon from '@mui/icons-material/Logout';
|
||||
import TuneIcon from '@mui/icons-material/Tune';
|
||||
|
||||
const PREFIX = 'TopAppBarVertical';
|
||||
|
||||
@@ -90,12 +92,14 @@ const openedMixin = (theme) => ({
|
||||
overflowX: 'hidden',
|
||||
borderRadius: "0 !important",
|
||||
border: "0px !important",
|
||||
backgroundColor: "unset !important",
|
||||
});
|
||||
const closedMixin = (theme) => ({
|
||||
overflowX: 'hidden',
|
||||
width: `calc(${theme.spacing(4)} + 1px)`,
|
||||
borderRadius: "0 !important",
|
||||
border: "0px !important",
|
||||
border: "0px !important",
|
||||
backgroundColor: "unset !important",
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
width: `calc(${theme.spacing(5)} + 1px)`,
|
||||
borderRadius: "0 !important",
|
||||
@@ -108,28 +112,44 @@ const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open'
|
||||
flexShrink: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
boxSizing: 'border-box',
|
||||
background: `linear-gradient(180deg, ${theme.topAppBarColor}, ${theme.topAppBarBottomColor}) !important`,
|
||||
backgroundColor: `unset !important`,
|
||||
[`& .${classes.listSubHeader}`]: {
|
||||
//color: ` ${theme.navBarTextIconColor} !important`,
|
||||
backgroundColor: `${theme.topAppBarColor} !important`,
|
||||
//backgroundColor: `${theme.topAppBarColor} !important`,
|
||||
backgroundColor: "unset !important",
|
||||
},
|
||||
[`& .${classes.listSubHeader}:hover`]: {
|
||||
backgroundColor: `${theme.topAppBarColor} !important`,
|
||||
//backgroundColor: `${theme.topAppBarColor} !important`,
|
||||
filter: `brightness(90%)`,
|
||||
color: `${theme.navBarTextIconColor} !important`,
|
||||
backgroundColor: "unset !important",
|
||||
},
|
||||
variants: [
|
||||
{
|
||||
props: ({ open }) => open,
|
||||
style: {
|
||||
...openedMixin(theme),
|
||||
'& .MuiDrawer-paper': openedMixin(theme),
|
||||
'& .MuiDrawer-paper': {
|
||||
...openedMixin(theme),
|
||||
backgroundColor: "unset !important",
|
||||
'& .MuiList-root': {
|
||||
backgroundColor: "unset !important",
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
props: ({ open }) => !open,
|
||||
style: {
|
||||
...closedMixin(theme),
|
||||
'& .MuiDrawer-paper': closedMixin(theme),
|
||||
'& .MuiDrawer-paper': {
|
||||
...closedMixin(theme),
|
||||
backgroundColor: "unset !important",
|
||||
'& .MuiList-root': {
|
||||
backgroundColor: "unset !important",
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -143,6 +163,7 @@ export const StyledListItem = styled(ListItem)(
|
||||
marginTop: 0,
|
||||
paddingBottom: "2px",
|
||||
color: theme.navBarTextColor,
|
||||
backgroundColor: "unset !important",
|
||||
}),
|
||||
);
|
||||
export const StyledListItemIcon = styled(ListItemIcon)(
|
||||
@@ -152,6 +173,7 @@ export const StyledListItemIcon = styled(ListItemIcon)(
|
||||
paddingBottom: 0,
|
||||
minWidth: "45px",
|
||||
//color: theme.navBarTextIconColor,
|
||||
backgroundColor: "unset !important",
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -166,7 +188,7 @@ query getGlobalSettings {
|
||||
const Dashboard = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} button component={Link} to='/new' key={"home"} >
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new' key={"home"} >
|
||||
<StyledListItemIcon >
|
||||
<MythicStyledTooltip title={"Operation Dashboard"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<SpaceDashboardTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement" />
|
||||
@@ -179,7 +201,7 @@ const Dashboard = () => {
|
||||
const ActiveCallbacks = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} button component={Link} to='/new/callbacks' key={"callbacks"} >
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/callbacks' key={"callbacks"} >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Active Callbacks"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<PhoneCallbackIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
@@ -193,7 +215,7 @@ const ActiveCallbacks = () => {
|
||||
const Payloads = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem button className={classes.listSubHeader} component={Link} to='/new/payloads' key={"payloads"} >
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/payloads' key={"payloads"} >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Payloads"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<FontAwesomeIcon style={{color: theme.navBarTextIconColor}} icon={faBiohazard} size="lg"/>
|
||||
@@ -206,7 +228,7 @@ const Payloads = () => {
|
||||
const SearchCallbacks = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem button className={classes.listSubHeader} component={Link} to='/new/search?tab=callbacks&searchField=Host&search=' >
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/search?tab=callbacks&searchField=Host&search=' >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Search Callbacks"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<PhoneCallbackIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
@@ -220,7 +242,7 @@ const SearchCallbacks = () => {
|
||||
const SearchTasks = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem button className={classes.listSubHeader} component={Link} to='/new/search?tab=tasks&searchField=Command+and+Parameters&search=&taskStatus=' >
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/search?tab=tasks&searchField=Command+and+Parameters&search=&taskStatus=' >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Search Tasks"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<AssignmentIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
@@ -233,7 +255,7 @@ const SearchTasks = () => {
|
||||
const SearchPayloads = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem button className={classes.listSubHeader} component={Link} to='/new/search?tab=payloads&searchField=Filename&search=&taskStatus=&c2=All+C2&payloadtype=All+Payload+Types'>
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/search?tab=payloads&searchField=Filename&search=&taskStatus=&c2=All+C2&payloadtype=All+Payload+Types'>
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Search Payloads"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<FontAwesomeIcon style={{color: theme.navBarTextIconColor}} size={"lg"} icon={faBiohazard} />
|
||||
@@ -247,7 +269,7 @@ const SearchPayloads = () => {
|
||||
const SearchFiles = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem button className={classes.listSubHeader} component={Link} to='/new/search?searchField=Filename&tab=files&location=Downloads&host=&search=' >
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/search?searchField=Filename&tab=files&location=Downloads&host=&search=' >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Search Files"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<AttachmentIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
@@ -260,7 +282,7 @@ const SearchFiles = () => {
|
||||
const SearchScreenshots = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem button className={classes.listSubHeader} component={Link} to='/new/search?searchField=Filename&tab=files&location=Screenshots' >
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/search?searchField=Filename&tab=files&location=Screenshots' >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Search Screenshots"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<CameraAltTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
@@ -273,7 +295,7 @@ const SearchScreenshots = () => {
|
||||
const SearchCredentials = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem button className={classes.listSubHeader} component={Link} to='/new/search?t?searchField=Account&tab=credentials&search='>
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/search?t?searchField=Account&tab=credentials&search='>
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Search Credentials"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<VpnKeyIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement" />
|
||||
@@ -286,7 +308,7 @@ const SearchCredentials = () => {
|
||||
const SearchKeylogs = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem button className={classes.listSubHeader} component={Link} to='/new/search?tab=keylogs&searchField=Host&search='>
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/search?tab=keylogs&searchField=Host&search='>
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Search Keylogs"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<KeyboardIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
@@ -299,7 +321,7 @@ const SearchKeylogs = () => {
|
||||
const SearchArtifacts = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem button className={classes.listSubHeader} component={Link} to='/new/search?tab=artifacts&searchField=Host&search=' >
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/search?tab=artifacts&searchField=Host&search=' >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Search Artifacts"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<FingerprintIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
@@ -312,7 +334,7 @@ const SearchArtifacts = () => {
|
||||
const SearchTokens = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem button className={classes.listSubHeader} component={Link} to='/new/search?tab=tokens&searchField=Host&search=' >
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/search?tab=tokens&searchField=Host&search=' >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Search Tokens"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<ConfirmationNumberIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
@@ -325,7 +347,7 @@ const SearchTokens = () => {
|
||||
const SearchProxies = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem button className={classes.listSubHeader} component={Link} to='/new/search?tab=socks'>
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/search?tab=socks'>
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Search Proxies"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<FontAwesomeIcon style={{color: theme.navBarTextIconColor}} size={"lg"} icon={faSocks} />
|
||||
@@ -338,7 +360,7 @@ const SearchProxies = () => {
|
||||
const SearchProcesses = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem button className={classes.listSubHeader} component={Link} to='/new/search?tab=processes&searchField=Name&search=&host=' >
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/search?tab=processes&searchField=Name&search=&host=' >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Search Processes"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<AccountTreeIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
@@ -351,7 +373,7 @@ const SearchProcesses = () => {
|
||||
const SearchTags = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem button className={classes.listSubHeader} component={Link} to='/new/search?tab=tags&searchField=TagType&search=&host='>
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/search?tab=tags&searchField=TagType&search=&host='>
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Search Tags"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<LocalOfferTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
@@ -365,7 +387,7 @@ const SearchTags = () => {
|
||||
const Mitre = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem button className={classes.listSubHeader} component={Link} to='/new/mitre' >
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/mitre' >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"MITRE ATT&CK"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<TableChartTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
@@ -378,7 +400,7 @@ const Mitre = () => {
|
||||
const Reporting = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem button className={classes.listSubHeader} component={Link} to='/new/reporting' >
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/reporting' >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Reporting"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<SportsScoreIcon style={{color: theme.navBarTextIconColor}} size={"medium"} />
|
||||
@@ -391,7 +413,7 @@ const Reporting = () => {
|
||||
const Tags = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem button className={classes.listSubHeader} component={Link} to='/new/tagtypes' >
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/tagtypes' >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Tags"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<LocalOfferTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
@@ -404,7 +426,7 @@ const Tags = () => {
|
||||
const Eventing = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem button className={classes.listSubHeader} component={Link} to='/new/eventing' >
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/eventing' >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Eventing"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<PlayCircleFilledTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
@@ -416,7 +438,7 @@ const Eventing = () => {
|
||||
}
|
||||
const JupyterNotebook = () => {
|
||||
return (
|
||||
<StyledListItem button className={classes.listSubHeader} target="_blank" component={Link} to='/jupyter' key={"jupyter"} >
|
||||
<StyledListItem className={classes.listSubHeader} target="_blank" component={Link} to='/jupyter' key={"jupyter"} >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Jupyter Notebooks"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<img src={JupyterLogo} height={"25px"} width={"25px"} />
|
||||
@@ -428,7 +450,7 @@ const JupyterNotebook = () => {
|
||||
}
|
||||
const GraphQL = () => {
|
||||
return (
|
||||
<StyledListItem button className={classes.listSubHeader} target="_blank" component={Link} to='/console' key={"console"} >
|
||||
<StyledListItem className={classes.listSubHeader} target="_blank" component={Link} to='/console' key={"console"} >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"GraphQL Console"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<img src={GraphQLLogo} height={"25px"} width={"25px"} className="mythicElement"/>
|
||||
@@ -441,7 +463,7 @@ const GraphQL = () => {
|
||||
const CreatePayload = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem button className={classes.listSubHeader} component={Link} to='/new/createpayload' key={"createpayload"} state={{from: 'TopAppBar'}}>
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/createpayload' key={"createpayload"} state={{from: 'TopAppBar'}}>
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Create Payload"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<FontAwesomeIcon style={{color: theme.navBarTextIconColor}} size={"lg"} icon={faBiohazard} />
|
||||
@@ -455,7 +477,7 @@ const CreatePayload = () => {
|
||||
const CreateWrapper = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem button className={classes.listSubHeader} component={Link} to='/new/createwrapper' key={"createwrapper"} >
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/createwrapper' key={"createwrapper"} >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Create Wrapper"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<PostAddIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
@@ -469,7 +491,7 @@ const CreateWrapper = () => {
|
||||
const PayloadTypesAndC2 = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem button className={classes.listSubHeader} component={Link} to='/new/payloadtypes' key={"payloadtypes"}>
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/payloadtypes' key={"payloadtypes"}>
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Installed Services"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<HeadsetTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
@@ -482,7 +504,7 @@ const PayloadTypesAndC2 = () => {
|
||||
const Operations = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem button className={classes.listSubHeader} component={Link} to='/new/operations' key={"modifyoperations"}>
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/operations' key={"modifyoperations"}>
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Modify Operations"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<EditIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
@@ -495,7 +517,7 @@ const Operations = () => {
|
||||
const BrowserScripts = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem button className={classes.listSubHeader} component={Link} to='/new/browserscripts' key={"browserscripts"} >
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/browserscripts' key={"browserscripts"} >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Browser Scripts"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<CodeOffIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
@@ -643,8 +665,8 @@ export function TopAppBarVertical(props) {
|
||||
const [serverName, setServerName] = React.useState("...");
|
||||
useQuery(GET_SETTINGS, {fetchPolicy: "no-cache",
|
||||
onCompleted: (data) => {
|
||||
setServerVersion(data.getGlobalSettings.settings["MYTHIC_SERVER_VERSION"]);
|
||||
setServerName(data.getGlobalSettings.settings["MYTHIC_GLOBAL_SERVER_NAME"]);
|
||||
setServerVersion(data.getGlobalSettings.settings["server_config"]["version"]);
|
||||
setServerName(data.getGlobalSettings.settings["server_config"]["name"]);
|
||||
}
|
||||
});
|
||||
const toggleDrawerOpen = (e) => {
|
||||
@@ -735,10 +757,12 @@ export function TopAppBarVertical(props) {
|
||||
return (
|
||||
<>
|
||||
{me?.user?.current_operation_id ? (<EventFeedNotifications me={me} />) : null }
|
||||
<Drawer anchor="left" variant="permanent" open={menuOpen} onClose={handleDrawerClose} >
|
||||
<Drawer anchor="left" variant="permanent" open={menuOpen} onClose={handleDrawerClose}
|
||||
style={{borderRight: "1px solid grey !important", backgroundColor: "unset !important"
|
||||
}}>
|
||||
<List style={{paddingTop: 0, marginTop: 0, height: "100%", display: "flex", flexDirection: "column",
|
||||
backgroundColor: theme.topAppBarColor,
|
||||
border: "unset !important"}}>
|
||||
backgroundColor: "unset !important",
|
||||
borderBottom: "unset !important", borderLeft: "unset !important", borderTop: "unset !important"}}>
|
||||
<ListItem className={classes.listSubHeader} style={{marginTop:0, paddingTop: 0, paddingLeft: "2px", paddingBottom: 0}}>
|
||||
<ListItemIcon >
|
||||
<img src={ReactLogo} onClick={()=>navigate('/new')} width={"40px"} height={"35px"}/>
|
||||
@@ -760,7 +784,7 @@ export function TopAppBarVertical(props) {
|
||||
</>
|
||||
} />
|
||||
</ListItem>
|
||||
<StyledListItem className={classes.listSubHeader} button onClick={toggleDrawerOpen} style={{height: "30px"}} >
|
||||
<StyledListItem className={classes.listSubHeader} onClick={toggleDrawerOpen} style={{height: "30px"}} >
|
||||
<StyledListItemIcon ><MenuIcon style={{color: theme.navBarTextIconColor}} onClick={toggleDrawerOpen} fontSize={"medium"} className="mythicElement" /></StyledListItemIcon>
|
||||
<ListItemText primary={
|
||||
<>
|
||||
@@ -795,7 +819,7 @@ export function TopAppBarVertical(props) {
|
||||
<div style={{flexGrow: 1, overflowY: "auto", overflowX: "hidden"}}>
|
||||
{getShortcuts({shortcuts: sideShortcuts})}
|
||||
<Divider style={{borderColor: "white"}} />
|
||||
<StyledListItem button className={classes.listSubHeader} onClick={handleToggleExtra}>
|
||||
<StyledListItem className={classes.listSubHeader} onClick={handleToggleExtra}>
|
||||
<StyledListItemIcon>
|
||||
<MoreHorizIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} />
|
||||
</StyledListItemIcon>
|
||||
@@ -876,16 +900,29 @@ function TopBarRightShortcutsVertical({me, menuOpen, serverName}){
|
||||
{
|
||||
name: (
|
||||
<>
|
||||
<Typography paragraph={true} variant="caption" style={{marginBottom: "0"}}>Signed in as:</Typography>
|
||||
<Typography paragraph={true} variant="body1" style={{marginBottom: "0", fontWeight: 600}}> {me?.user?.username || "" } </Typography>
|
||||
</>
|
||||
<Typography variant={"caption"} style={{marginBottom: "0"}}>Signed in as:</Typography>
|
||||
<Typography variant={"body1"} style={{marginBottom: "0", fontWeight: 600}}>{me?.user?.username}</Typography>
|
||||
</>),
|
||||
disabled: true,
|
||||
click: handleSettingsClose
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div style={{display: "flex", alignItems: "center"}}>
|
||||
<TuneIcon style={{marginRight: "0.5rem"}}/>
|
||||
<Typography variant="body1" style={{marginBottom: "0", }}> Settings </Typography>
|
||||
</div>
|
||||
),
|
||||
to: "/new/settings",
|
||||
component: Link,
|
||||
click: handleSettingsClose
|
||||
},
|
||||
{
|
||||
name: "Logout",
|
||||
name: (
|
||||
<div style={{display: "flex", alignItems: "center"}}>
|
||||
<LogoutIcon style={{marginRight: "0.5rem"}} /> Logout
|
||||
</div>
|
||||
),
|
||||
click: handleLogout,
|
||||
to: "",
|
||||
}
|
||||
@@ -961,7 +998,7 @@ function TopBarRightShortcutsVertical({me, menuOpen, serverName}){
|
||||
/>
|
||||
</ClickAwayListener>
|
||||
}
|
||||
<StyledListItem button className={classes.listSubHeader} onClick={() => setOpenFeedbackForm(true)} >
|
||||
<StyledListItem className={classes.listSubHeader} onClick={() => setOpenFeedbackForm(true)} >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Submit feedback via Webhook"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<ThumbDownTwoTone style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement" />
|
||||
@@ -977,7 +1014,7 @@ function TopBarRightShortcutsVertical({me, menuOpen, serverName}){
|
||||
onClose={()=>{setOpenFeedbackForm(false);}} />}
|
||||
/>
|
||||
}
|
||||
<StyledListItem button className={classes.listSubHeader} onClick={handleDocumentationMenu} >
|
||||
<StyledListItem className={classes.listSubHeader} onClick={handleDocumentationMenu} >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Documentation Links"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<HelpTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
@@ -987,7 +1024,7 @@ function TopBarRightShortcutsVertical({me, menuOpen, serverName}){
|
||||
<ListItemText primary={"Help"} />
|
||||
</StyledListItem>
|
||||
|
||||
<StyledListItem button className={classes.listSubHeader} component={Link} to='/new/EventFeed' >
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/EventFeed' >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Event Feed"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<TopAppBarVerticalEventLogNotifications />
|
||||
@@ -996,7 +1033,7 @@ function TopBarRightShortcutsVertical({me, menuOpen, serverName}){
|
||||
<ListItemText primary={"Event Feed"} />
|
||||
</StyledListItem>
|
||||
|
||||
<StyledListItem button className={classes.listSubHeader} onClick={handleSettingsMenu} >
|
||||
<StyledListItem className={classes.listSubHeader} onClick={handleSettingsMenu} >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"User Settings"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<ManageAccountsTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement" />
|
||||
|
||||
@@ -4,6 +4,7 @@ import {BrowserScriptsTable} from './BrowserScriptsTable';
|
||||
import {snackActions} from '../../utilities/Snackbar';
|
||||
import { Backdrop } from '@mui/material';
|
||||
import {CircularProgress} from '@mui/material';
|
||||
import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
|
||||
|
||||
|
||||
const SUB_BrowserScripts = gql`
|
||||
@@ -135,7 +136,7 @@ export function BrowserScripts({me}){
|
||||
}
|
||||
}, [])
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicPageBody>
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={false}>
|
||||
<CircularProgress color="inherit" />
|
||||
</Backdrop>
|
||||
@@ -145,6 +146,6 @@ export function BrowserScripts({me}){
|
||||
onSubmitEdit={onSubmitEdit} onRevert={onRevert} onSubmitNew={onSubmitCreateNewBrowserScript}
|
||||
/>
|
||||
|
||||
</React.Fragment>
|
||||
</MythicPageBody>
|
||||
);
|
||||
}
|
||||
@@ -13,31 +13,28 @@ import AddCircleIcon from '@mui/icons-material/AddCircle';
|
||||
import Button from '@mui/material/Button';
|
||||
import { MythicDialog } from '../../MythicComponents/MythicDialog';
|
||||
import {EditScriptDialog} from './EditScriptDialog';
|
||||
import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
|
||||
|
||||
|
||||
export function BrowserScriptsTable(props){
|
||||
const theme = useTheme();
|
||||
const [openNewScriptDialog, setOpenNewScriptDialog] = React.useState(false);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main}} variant={"elevation"}>
|
||||
<Typography variant="h5" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
|
||||
Browser Scripts
|
||||
</Typography>
|
||||
<Button size="small" onClick={() => setOpenNewScriptDialog(true)} style={{float: "right", marginRight: "30px", color: "white"}}
|
||||
<>
|
||||
<MythicPageHeader title={"Browser Scripts"}>
|
||||
<Button size="small" onClick={() => setOpenNewScriptDialog(true)} style={{whiteSpace: "nowrap", color: "white"}}
|
||||
startIcon={<AddCircleIcon color={"success"} style={{backgroundColor: "white", borderRadius: "10px"}}/>} >
|
||||
New Script
|
||||
</Button>
|
||||
{openNewScriptDialog ? (
|
||||
{openNewScriptDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openNewScriptDialog}
|
||||
onClose={()=>{setOpenNewScriptDialog(false);}}
|
||||
innerDialog={
|
||||
<EditScriptDialog me={props.me} onClose={()=>{setOpenNewScriptDialog(false);}} title="Create New Browser Script" new={true} onSubmitEdit={props.onSubmitNew} />
|
||||
} />
|
||||
) : null
|
||||
} />
|
||||
}
|
||||
</Paper>
|
||||
<TableContainer className="mythicElement" style={{maxHeight: "90%"}}>
|
||||
</MythicPageHeader>
|
||||
<TableContainer className="mythicElement" style={{height: "100%", flexGrow: 1}}>
|
||||
<Table stickyHeader={true} size="small" style={{"tableLayout": "fixed", "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
@@ -61,8 +58,8 @@ export function BrowserScriptsTable(props){
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</React.Fragment>
|
||||
</TableContainer>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import CardHeader from '@mui/material/CardHeader';
|
||||
import {gql, useQuery} from '@apollo/client';
|
||||
import { CardContent } from '@mui/material';
|
||||
import {classes, StyledButton, StyledDivider} from '../../MythicComponents/MythicTransferList';
|
||||
import {MythicDraggableDialogTitle} from "../../MythicComponents/MythicDraggableDialogTitle";
|
||||
|
||||
const getCommandsQuery = gql`
|
||||
query GetCallbackDetails($callback_id: Int!) {
|
||||
@@ -184,8 +185,8 @@ export function AddRemoveCallbackCommandsDialog(props) {
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Add or Remove Commands for Callback {props.display_id} </DialogTitle>
|
||||
<DialogContent dividers={true} style={{height: "100%", display: "flex", flexDirection: "column", position: "relative", maxHeight: "100%"}}>
|
||||
<DialogTitle>Add or Remove Commands for Callback {props.display_id} </DialogTitle>
|
||||
<DialogContent dividers={true} style={{height: "100%", display: "flex", flexDirection: "column", position: "relative", overflowY: "auto"}}>
|
||||
This will add or remove commands associated with this callback from Mythic's perspective.
|
||||
This does NOT add or remove commands within the payload itself that's beaconing out to Mythic.
|
||||
<div style={{display: "flex", flexDirection: "row", overflowY: "auto", flexGrow: 1, minHeight: 0}}>
|
||||
|
||||
@@ -11,7 +11,7 @@ import FormControl from '@mui/material/FormControl';
|
||||
import Select from '@mui/material/Select';
|
||||
import { Typography } from '@mui/material';
|
||||
import { ReactFlow,
|
||||
applyEdgeChanges,
|
||||
EdgeLabelRenderer,getBezierPath, BaseEdge,
|
||||
Handle, Position, useReactFlow, ReactFlowProvider, Panel,
|
||||
MiniMap, Controls, ControlButton, useUpdateNodeInternals,
|
||||
getConnectedEdges, useNodesState, useEdgesState
|
||||
@@ -37,6 +37,7 @@ import SendIcon from '@mui/icons-material/Send';
|
||||
import {getIconName} from "./ResponseDisplayTable";
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import RestartAltIcon from '@mui/icons-material/RestartAlt';
|
||||
import { MythicAgentSVGIconNoTooltip} from "../../MythicComponents/MythicAgentSVGIcon";
|
||||
|
||||
const ITEM_HEIGHT = 48;
|
||||
const ITEM_PADDING_TOP = 8;
|
||||
@@ -459,6 +460,41 @@ export function GroupNode({data}) {
|
||||
}
|
||||
const nodeTypes = { "agentNode": AgentNode, "groupNode": GroupNode, "taskNode": TaskNode, "browserscriptNode": BrowserscriptNode };
|
||||
|
||||
export function C2LabelEdge({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, data,
|
||||
labelStyle, markerStart, markerEnd, labelShowBg, labelBgStyle, style, label,
|
||||
interactionWidth
|
||||
}){
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<BaseEdge id={id} path={edgePath} labelStyle={labelStyle} markerEnd={markerEnd} markerStart={markerStart}
|
||||
labelShowBg={labelShowBg} labelBgStyle={labelBgStyle} style={style} label={label}
|
||||
interactionWidth={interactionWidth} labelX={labelX} labelY={labelY}/>
|
||||
<EdgeLabelRenderer>
|
||||
<div
|
||||
style={{ position: "absolute",
|
||||
transform: `translate(-40%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||
}}
|
||||
className="nodrag nopan"
|
||||
>
|
||||
{data.has_logo && <MythicAgentSVGIconNoTooltip payload_type={label}
|
||||
is_p2p={data.is_p2p}
|
||||
className={"circleImageNode"}/>}
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
const edgeTypes = {
|
||||
C2IconEdge: C2LabelEdge
|
||||
};
|
||||
const elk = new ELK();
|
||||
const getWidth = (node) => {
|
||||
if(node.type === "taskNode"){
|
||||
@@ -854,9 +890,12 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
|
||||
mythic_id: edge.id,
|
||||
source: `${edge.source.id}`,
|
||||
target: "Mythic",
|
||||
label: edge.c2profile.name,
|
||||
type: "C2IconEdge",
|
||||
animated: true,
|
||||
label: edge.c2profile.name,
|
||||
data: {
|
||||
has_logo: edge.c2profile.has_logo,
|
||||
is_p2p: edge.c2profile.is_p2p,
|
||||
source: {...edge.source, parentId: getGroupBy(edge.source, view_config)},
|
||||
target: {parentId: "Mythic"},
|
||||
end_timestamp: edge.end_timestamp,
|
||||
@@ -970,8 +1009,11 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
|
||||
target: `${edge.source.id}`,
|
||||
label: edge.c2profile.name,
|
||||
animated: true,
|
||||
type: "C2IconEdge",
|
||||
data: {
|
||||
end_timestamp: edge.end_timestamp,
|
||||
has_logo: edge.c2profile.has_logo,
|
||||
is_p2p: edge.c2profile.is_p2p,
|
||||
source: {...edge.destination, parentId: getGroupBy(edge.destination, view_config)},
|
||||
target: {...edge.source, parentId: getGroupBy(edge.source, view_config)},
|
||||
}
|
||||
@@ -1004,8 +1046,11 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
|
||||
target: `${edge.destination.id}`,
|
||||
label: edge.c2profile.name,
|
||||
animated: true,
|
||||
type: "C2IconEdge",
|
||||
data: {
|
||||
end_timestamp: edge.end_timestamp,
|
||||
has_logo: edge.c2profile.has_logo,
|
||||
is_p2p: edge.c2profile.is_p2p,
|
||||
source: {...edge.source, parentId: getGroupBy(edge.source, view_config)},
|
||||
target: {...edge.target, parentId: getGroupBy(edge.target, view_config)},
|
||||
}
|
||||
@@ -1040,8 +1085,11 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
|
||||
target: `${edge.destination.id}`,
|
||||
label: edge.c2profile.name,
|
||||
animated: true,
|
||||
type: "C2IconEdge",
|
||||
data: {
|
||||
end_timestamp: edge.end_timestamp,
|
||||
has_logo: edge.c2profile.has_logo,
|
||||
is_p2p: edge.c2profile.is_p2p,
|
||||
source: {...edge.source, parentId: getGroupBy(edge.source, view_config)},
|
||||
target: {...edge.target, parentId: getGroupBy(edge.destination, view_config)},
|
||||
}
|
||||
@@ -1239,7 +1287,7 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
|
||||
tempEdges[i].markerEnd.type = "arrowclosed"
|
||||
tempEdges[i].labelBgStyle = {
|
||||
fill: theme.tableHover,
|
||||
fillOpacity: 0.6,
|
||||
fillOpacity: 0.8,
|
||||
}
|
||||
tempEdges[i].labelStyle = {
|
||||
fill: theme.palette.background.contrast,
|
||||
@@ -1270,6 +1318,7 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
|
||||
target: tempEdges[j].data.target.parentId,
|
||||
label: "",
|
||||
hidden: true,
|
||||
type: "C2IconEdge",
|
||||
data: {
|
||||
source: {parentId: parentIds[i].id},
|
||||
target: tempEdges[j].data.target,
|
||||
@@ -1289,6 +1338,7 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
|
||||
target: tempEdges[j].data.target.parentId,
|
||||
label: "",
|
||||
hidden: true,
|
||||
type: "C2IconEdge",
|
||||
data: {
|
||||
source: {parentId: parentIds[i].id},
|
||||
target: tempEdges[j].data.target,
|
||||
@@ -1368,6 +1418,7 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
onPaneClick={onPaneClick}
|
||||
onNodeContextMenu={onNodeContextMenu}
|
||||
onNodeClick={onNodeSelected}
|
||||
|
||||
@@ -106,6 +106,7 @@ export const taskingDataFragment = gql`
|
||||
domain
|
||||
ip
|
||||
mythictree_groups
|
||||
pid
|
||||
}
|
||||
callback_id
|
||||
commentOperator{
|
||||
@@ -146,8 +147,16 @@ export const taskingDataFragment = gql`
|
||||
command_name
|
||||
opsec_pre_blocked
|
||||
opsec_pre_bypassed
|
||||
opsec_pre_bypass_user {
|
||||
id
|
||||
username
|
||||
}
|
||||
opsec_post_blocked
|
||||
opsec_post_bypassed
|
||||
opsec_post_bypass_user {
|
||||
id
|
||||
username
|
||||
}
|
||||
is_interactive_task
|
||||
interactive_task_type
|
||||
has_intercepted_response
|
||||
@@ -167,6 +176,7 @@ export const taskingDataFragment = gql`
|
||||
domain
|
||||
ip
|
||||
mythictree_groups
|
||||
pid
|
||||
}
|
||||
completed
|
||||
subtask_group_name
|
||||
|
||||
@@ -19,8 +19,12 @@ export const getCallbackIdFromClickedTab = (tabId) => {
|
||||
return Number(tabId.split("fileBrowser")[0]);
|
||||
}else if(tabId.includes("interact")){
|
||||
return Number(tabId.split("interact")[0]);
|
||||
}else if(tabId.includes("processBrowser")){
|
||||
}else if(tabId.includes("processBrowser")) {
|
||||
return Number(tabId.split("processBrowser")[0]);
|
||||
}else if(tabId.includes("customFileBasedBrowser")) {
|
||||
return Number(tabId.split("customFileBasedBrowser")[0]);
|
||||
}else if(tabId.includes("customProcessBasedBrowser")) {
|
||||
return Number(tabId.split("customProcessBasedBrowser")[0]);
|
||||
} else {
|
||||
console.log("unknown tab type", tabId);
|
||||
return 0;
|
||||
@@ -30,6 +34,7 @@ export const getCallbackIdFromClickedTab = (tabId) => {
|
||||
export function Callbacks({me}) {
|
||||
const [openCallbackImport, setOpenCallbackImport] = React.useState(false);
|
||||
const [topDisplay, setTopDisplay] = React.useState('table');
|
||||
const [newDataForTab, setNewDataForTab] = React.useState({});
|
||||
const [openTabs, setOpenTabs] = React.useState([]);
|
||||
const [clickedTabId, setClickedTabIdValue] = React.useState('');
|
||||
const openTabRef = React.useRef([]);
|
||||
@@ -46,6 +51,7 @@ export function Callbacks({me}) {
|
||||
}
|
||||
}
|
||||
setClickedTabIdValue(tabID);
|
||||
setNewDataForTab((prev) => {return {...prev, [tabID]: false}});
|
||||
}
|
||||
useEffect(() => {
|
||||
const oldTabs = localStorage.getItem('openTabs');
|
||||
@@ -210,7 +216,8 @@ export function Callbacks({me}) {
|
||||
onDragEnd={(sizes) => localStorage.setItem('callbackTableSplitSizes', JSON.stringify(sizes))}
|
||||
style={{ height: "100%" }}>
|
||||
<div style={{display: "flex", flexDirection: "row-reverse"}}>
|
||||
<Paper elevation={5} style={{width: "30px", display: "flex", flexDirection: "column", alignItems: "center", overflow: "hidden"}}>
|
||||
<Paper elevation={5} style={{width: "30px", display: "flex", flexDirection: "column", alignItems: "center", overflow: "hidden",
|
||||
backgroundColor: "transparent"}}>
|
||||
{topDisplay !== 'table' &&
|
||||
<MythicStyledTooltip title={"Table View"}>
|
||||
<IconButton onClick={() =>setTopDisplay("table")}>
|
||||
@@ -256,87 +263,11 @@ export function Callbacks({me}) {
|
||||
me={me}
|
||||
onDragEnd={onDragEnd}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
newDataForTab={newDataForTab}
|
||||
setNewDataForTab={setNewDataForTab}
|
||||
/>
|
||||
</div>
|
||||
</Split>
|
||||
</>
|
||||
);
|
||||
}
|
||||
/*
|
||||
function SpeedDialWrapperPreMemo({ setTopDisplay }) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [openCallbackImport, setOpenCallbackImport] = React.useState(false);
|
||||
const actions = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
icon: <TocIcon />,
|
||||
name: 'Table layout',
|
||||
onClick: () => {
|
||||
setTopDisplay('table');
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <AssessmentIcon />,
|
||||
name: 'Graph layout',
|
||||
onClick: () => {
|
||||
setTopDisplay('graph');
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <PhoneForwardedIcon />,
|
||||
name: "Import Callback",
|
||||
onClick: () => {
|
||||
setOpenCallbackImport(true);
|
||||
}
|
||||
}
|
||||
],
|
||||
[] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
);
|
||||
return (
|
||||
<React.Fragment>
|
||||
{openCallbackImport &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openCallbackImport}
|
||||
onClose={()=>{setOpenCallbackImport(false);}}
|
||||
innerDialog={<ImportCallbackConfigDialog onClose={()=>{setOpenCallbackImport(false);}} />}
|
||||
/>
|
||||
}
|
||||
<StyledSpeedDial
|
||||
ariaLabel='SpeedDial example'
|
||||
className={classes.speedDial}
|
||||
icon={<SpeedDialIcon />}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
onOpen={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
FabProps={{
|
||||
color: 'info', size: "small", variant: "extended",
|
||||
sx: {
|
||||
height: "25px", minWidth: "unset", width: "25px"
|
||||
}
|
||||
}}
|
||||
open={open}
|
||||
style={{ marginTop:"10px", marginRight: "20px"}}
|
||||
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,5 +1,5 @@
|
||||
import React, { useEffect, useMemo, useContext} from 'react';
|
||||
import {MythicDialog} from '../../MythicComponents/MythicDialog';
|
||||
import {MythicDialog, MythicModifyStringDialog} from '../../MythicComponents/MythicDialog';
|
||||
import {
|
||||
exportCallbackConfigQuery,
|
||||
hideCallbackMutation, lockCallbackMutation, unlockCallbackMutation, updateCallbackTriggerMutation,
|
||||
@@ -7,20 +7,19 @@ import {
|
||||
updateSleepInfoCallbackMutation
|
||||
} from './CallbackMutations';
|
||||
import {snackActions} from '../../utilities/Snackbar';
|
||||
import {useMutation, useLazyQuery } from '@apollo/client';
|
||||
import {useMutation, useLazyQuery, gql } from '@apollo/client';
|
||||
import {
|
||||
CallbacksTableIDCell,
|
||||
CallbacksTableStringCell,
|
||||
CallbacksTableLastCheckinCell,
|
||||
CallbacksTablePayloadTypeCell,
|
||||
CallbacksTableC2Cell,
|
||||
CallbacksTableOSCell,
|
||||
CallbacksTableSleepCell,
|
||||
CallbacksTableIPCell
|
||||
CallbacksTableIDCell,
|
||||
CallbacksTableStringCell,
|
||||
CallbacksTableLastCheckinCell,
|
||||
CallbacksTablePayloadTypeCell,
|
||||
CallbacksTableC2Cell,
|
||||
CallbacksTableOSCell,
|
||||
CallbacksTableSleepCell,
|
||||
CallbacksTableIPCell, CallbacksTableTagsCell
|
||||
} from './CallbacksTableRow';
|
||||
import MythicResizableGrid from '../../MythicComponents/MythicResizableGrid';
|
||||
import {TableFilterDialog} from './TableFilterDialog';
|
||||
import {CallbacksTabsHideMultipleDialog} from "./CallbacksTabsHideMultipleDialog";
|
||||
import {CallbacksTabsHideMultipleDialog, CallbacksTabsSelectMultipleDialog} from "./CallbacksTabsHideMultipleDialog";
|
||||
import {CallbacksTabsTaskMultipleDialog} from "./CallbacksTabsTaskMultipleDialog";
|
||||
import ip6 from 'ip6';
|
||||
import {CallbacksContext, OnOpenTabContext, OnOpenTabsContext} from "./CallbacksTop";
|
||||
@@ -53,7 +52,29 @@ import NotificationsActiveTwoToneIcon from '@mui/icons-material/NotificationsAct
|
||||
import NotificationsOffTwoToneIcon from '@mui/icons-material/NotificationsOffTwoTone';
|
||||
import {CallbacksTableEditTriggerOnCheckinDialog} from "./CallbacksTableEditTriggerOnCheckinDialog";
|
||||
import {CallbacksTableColumnsReorderDialog} from "./CallbacksTableColumnsReorderDialog";
|
||||
import PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwoTone';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import {EventTriggerContextSelectDialog} from "../Eventing/EventTriggerContextSelect";
|
||||
import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery";
|
||||
import {faFilter} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export const getCustomBrowsersQuery = gql(`
|
||||
query getCustomBrowsersQuery{
|
||||
custombrowser(where: {deleted: {_eq: false}}, order_by: {name: asc}){
|
||||
id
|
||||
name
|
||||
type
|
||||
separator
|
||||
columns
|
||||
default_visible_columns
|
||||
indicate_partial_listing
|
||||
show_current_path
|
||||
extra_table_inputs
|
||||
row_actions
|
||||
export_function
|
||||
}
|
||||
}
|
||||
`);
|
||||
export const ipCompare = (a, b) => {
|
||||
let aJSON = JSON.parse(a);
|
||||
if(aJSON.length === 0){return 0}
|
||||
@@ -109,6 +130,7 @@ const callbackTableInitialColumns = [
|
||||
{key: "agent", type: 'agent', name: "Agent", width: 150},
|
||||
{key: "c2", type: 'string', name: "C2", width: 45, disableSort: true, disableFilterMenu: true, disableDoubleClick: true},
|
||||
{key: "process_short_name", type: 'string', name: "Process Name", fillWidth: true},
|
||||
{key: "tags", type: 'tags', name: "Tags", fillWidth: false, disableDoubleClick: true, width: 150}
|
||||
];
|
||||
function CallbacksTablePreMemo(props){
|
||||
const callbacks = useContext(CallbacksContext);
|
||||
@@ -202,7 +224,7 @@ function CallbacksTablePreMemo(props){
|
||||
const [selectedColumn, setSelectedColumn] = React.useState({});
|
||||
const [columnVisibility, setColumnVisibility] = React.useState(() => {
|
||||
let defaults = {"visible": ["Interact", "Host", "Domain", "User", "Description", "Last Checkin", "Agent", "IP", "PID"],
|
||||
"hidden": ["Arch", "Sleep", "Process Name", "External IP", "C2", "OS", "Groups"]}
|
||||
"hidden": ["Arch", "Sleep", "Process Name", "External IP", "C2", "OS", "Groups", "Tags"]}
|
||||
try {
|
||||
const storageItem = GetMythicSetting({setting_name: "callbacks_table_columns", default_value: operatorSettingDefaults.callbacks_table_columns});
|
||||
|
||||
@@ -294,13 +316,13 @@ function CallbacksTablePreMemo(props){
|
||||
const ele = document.getElementById("download_config");
|
||||
if(ele !== null){
|
||||
ele.href = URL.createObjectURL(dataBlob);
|
||||
ele.download = data.exportCallbackConfig.agent_callback_id + ".json";
|
||||
ele.download = "exported_callback.json";
|
||||
ele.click();
|
||||
}else{
|
||||
const element = document.createElement("a");
|
||||
element.id = "download_config";
|
||||
element.href = URL.createObjectURL(dataBlob);
|
||||
element.download = data.exportCallbackConfig.agent_callback_id + ".json";
|
||||
element.download = "exported_callback.json";
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
}
|
||||
@@ -312,15 +334,22 @@ function CallbacksTablePreMemo(props){
|
||||
console.log(data);
|
||||
snackActions.error("Failed to export configuration: " + data.message)
|
||||
}
|
||||
})
|
||||
});
|
||||
const getCustomBrowsers = useMythicLazyQuery(getCustomBrowsersQuery, {fetchPolicy: "no-cache"});
|
||||
const taskingData = React.useRef({"parameters": "", "ui_feature": "callback_table:exit"});
|
||||
const [openTaskingButton, setOpenTaskingButton] = React.useState(false);
|
||||
const eventingDataRef = React.useRef({});
|
||||
const [openEventingDialog, setOpenEventingDialog] = React.useState(false);
|
||||
const [openEventingMultipleDialog, setOpenEventingMultipleDialog] = React.useState(false);
|
||||
const [updateSetting] = useSetMythicSetting();
|
||||
const onUpdateTrigger = (newTriggerValue) => {
|
||||
updateTrigger({variables: {callback_display_id: openTriggerDialog.display_id, trigger_on_checkin_after_time: newTriggerValue}})
|
||||
setOpenTriggerDialog({...openTriggerDialog, open: false});
|
||||
}
|
||||
const onRowContextClick = ({rowDataStatic}) => {
|
||||
async function getCustomBrowsersForContextMenu(){
|
||||
return await getCustomBrowsers({}).then(result => {return result.data?.custombrowser});
|
||||
}
|
||||
async function onRowContextClick({rowDataStatic}) {
|
||||
// based on row, return updated options array?
|
||||
let defaultInteractIcon = <KeyboardIcon style={{paddingRight: "5px"}}/>;
|
||||
if(interactType === "interactSplit"){
|
||||
@@ -328,9 +357,36 @@ function CallbacksTablePreMemo(props){
|
||||
} else if(interactType === "interactConsole"){
|
||||
defaultInteractIcon = <TerminalIcon style={{paddingRight: "5px"}}/>;
|
||||
}
|
||||
let customBrowsers = await getCustomBrowsersForContextMenu();
|
||||
customBrowsers = customBrowsers.map(b => {
|
||||
return {
|
||||
name: b.name,
|
||||
icon: b.type === 'file' ? <FontAwesomeIcon icon={faFolderOpen} style={{color: theme.folderColor, cursor: "pointer", marginRight: "10px"}} /> : <AccountTreeIcon style={{paddingRight: "5px"}}/>,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const tabType = b.type === 'file' ? 'customFileBasedBrowser' : 'customProcessBasedBrowser';
|
||||
onOpenTab({
|
||||
tabType: tabType,
|
||||
tabID: rowDataStatic.id + tabType + b.name,
|
||||
callbackID: rowDataStatic.id,
|
||||
color: rowDataStatic.color,
|
||||
displayID: rowDataStatic.display_id,
|
||||
customBrowser: b
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
return [
|
||||
{
|
||||
name: "Callback: " + rowDataStatic.display_id,
|
||||
name: <div style={{display: "flex", flexDirection: "column"}}>
|
||||
<Typography>
|
||||
{"Callback: " + rowDataStatic.display_id}
|
||||
</Typography>
|
||||
<Typography style={{fontSize: theme?.typography.pxToRem(12),}}>
|
||||
{rowDataStatic.user + "@" + rowDataStatic.host}
|
||||
</Typography>
|
||||
</div>,
|
||||
icon: null, click: ({event}) => {},
|
||||
type: "item",
|
||||
disabled: true
|
||||
@@ -398,12 +454,28 @@ function CallbacksTablePreMemo(props){
|
||||
setOpenTriggerDialog({open: true, trigger_on_checkin_after_time: rowDataStatic.trigger_on_checkin_after_time, display_id: rowDataStatic.display_id})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Eventing", icon: <PlayCircleFilledTwoToneIcon />, click: (event) => {}, type: "menu",
|
||||
menuItems: [
|
||||
{
|
||||
name: "Start Eventing Workflow", icon: <PlayCircleFilledTwoToneIcon style={{paddingRight: "5px"}} />,
|
||||
click: ({event}) => {
|
||||
eventingDataRef.current = {
|
||||
name: "callback_id",
|
||||
value: rowDataStatic.id
|
||||
};
|
||||
setOpenEventingDialog(true);
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Browsers", icon: null, click: () => {}, type: "menu",
|
||||
menuItems: [
|
||||
{
|
||||
name: 'File Browser', icon: <FontAwesomeIcon icon={faFolderOpen} style={{color: theme.folderColor, cursor: "pointer", marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const tabType = "fileBrowser";
|
||||
onOpenTab({
|
||||
@@ -417,6 +489,7 @@ function CallbacksTablePreMemo(props){
|
||||
{
|
||||
name: 'Process Browser', icon: <AccountTreeIcon style={{paddingRight: "5px"}}/>,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const tabType = "processBrowser";
|
||||
onOpenTab({
|
||||
@@ -427,6 +500,17 @@ function CallbacksTablePreMemo(props){
|
||||
displayID: rowDataStatic.display_id});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: <div style={{display: "flex", flexDirection: "column", width: "100%"}}>
|
||||
<Typography>
|
||||
{"Custom Agent Browsers:"}
|
||||
</Typography>
|
||||
</div>,
|
||||
icon: null, click: ({event}) => {},
|
||||
type: "item",
|
||||
disabled: true
|
||||
},
|
||||
...customBrowsers
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -444,6 +528,12 @@ function CallbacksTablePreMemo(props){
|
||||
setOpenTaskMultipleDialog({open: true, data: rowDataStatic});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Start Multiple Eventing Workflows", icon: <PlayCircleFilledTwoToneIcon style={{paddingRight: "5px"}}/>,
|
||||
click: ({event}) => {
|
||||
setOpenEventingMultipleDialog(true);
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -548,10 +638,10 @@ function CallbacksTablePreMemo(props){
|
||||
}
|
||||
];
|
||||
}
|
||||
const callbackDropdown = ({rowDataStatic, event}) => {
|
||||
callbackDropdownRef.current.options = onRowContextClick({rowDataStatic});
|
||||
async function callbackDropdown({rowDataStatic, event}) {
|
||||
callbackDropdownRef.current.options = await onRowContextClick({rowDataStatic});
|
||||
callbackDropdownRef.current.callback = rowDataStatic;
|
||||
callbackDropdownRef.current.dropdownAnchorRef = event.currentTarget;
|
||||
callbackDropdownRef.current.dropdownAnchorRef = event?.currentTarget || event.target;
|
||||
setOpenCallbackDropdown(true);
|
||||
}
|
||||
React.useEffect( () => {
|
||||
@@ -621,16 +711,16 @@ function CallbacksTablePreMemo(props){
|
||||
return;
|
||||
}
|
||||
if (!column.key) {
|
||||
setSortData({"sortKey": null, "sortType":null, "sortDirection": "ASC"});
|
||||
setSortData({sortKey: null, sortType:null, sortDirection: "ASC"});
|
||||
}
|
||||
if (sortData.sortKey === column.key) {
|
||||
if (sortData.sortDirection === 'ASC') {
|
||||
setSortData({...sortData, "sortDirection": "DESC"});
|
||||
setSortData({...sortData, sortDirection: "DESC"});
|
||||
} else {
|
||||
setSortData({"sortKey": null, "sortType":null, "sortDirection": "ASC"});
|
||||
setSortData({sortKey: null, sortType:null, sortDirection: "ASC"});
|
||||
}
|
||||
} else {
|
||||
setSortData({"sortKey": column.key, "sortType":column.type, "sortDirection": "ASC"});
|
||||
setSortData({sortKey: column.key, sortType:column.type, sortDirection: "ASC"});
|
||||
}
|
||||
};
|
||||
const onRowDoubleClick = React.useCallback( (event, rowID, rowDataStatic) => {
|
||||
@@ -644,23 +734,28 @@ function CallbacksTablePreMemo(props){
|
||||
}, []);
|
||||
const contextMenuOptions = [
|
||||
{
|
||||
name: 'Filter Column',
|
||||
name: 'Filter Column', type: "item",
|
||||
icon: <FontAwesomeIcon icon={faFilter} style={{paddingRight: "5px"}} />,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
if(columns[columnIndex].disableFilterMenu){
|
||||
snackActions.warning("Can't filter that column");
|
||||
return;
|
||||
}
|
||||
setSelectedColumn(columns[columnIndex]);
|
||||
setOpenContextMenu(true);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Reorder Columns and Adjust Visibility",
|
||||
name: "Reorder Columns and Adjust Visibility", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
setOpenReorderDialog(true);
|
||||
}
|
||||
}
|
||||
@@ -764,7 +859,7 @@ function CallbacksTablePreMemo(props){
|
||||
rowStyle: {backgroundColor: `${row.color}`},
|
||||
}} />;
|
||||
case "User":
|
||||
return <CallbacksTableStringCell key={`callback${row.id}_${c.name}`} cellData={row.user}
|
||||
return <CallbacksTableStringCell key={`callback${row.id}_${c.name}`} cellData={row.user + (row.impersonation_context === "" ? "" : ` [${row.impersonation_context}]`)}
|
||||
rowData={{...row,
|
||||
selected: row.id === clickedCallbackID,
|
||||
rowStyle: {backgroundColor: `${row.color}`},
|
||||
@@ -835,15 +930,22 @@ function CallbacksTablePreMemo(props){
|
||||
selected: row.id === clickedCallbackID,
|
||||
rowStyle: {backgroundColor: `${row.color}`},
|
||||
}} />;
|
||||
case "Tags":
|
||||
return <CallbacksTableTagsCell key={`callback${row.id}_${c.name}`}
|
||||
cellData={row.tags}
|
||||
rowData={{...row,
|
||||
selected: row.id === clickedCallbackID,
|
||||
rowStyle: {backgroundColor: `${row.color}`},
|
||||
}} />;
|
||||
}
|
||||
})];
|
||||
}
|
||||
}, [])
|
||||
}, [callbacks, sortData, filterOptions, columnVisibility, clickedCallbackID]);
|
||||
const onSubmitFilterOptions = (newFilterOptions) => {
|
||||
setFilterOptions(newFilterOptions);
|
||||
const onSubmitFilterOptions = (value) => {
|
||||
setFilterOptions({...filterOptions, [selectedColumn.key]: value });
|
||||
try{
|
||||
updateSetting({setting_name: "callbacks_table_filter_options", value: newFilterOptions});
|
||||
updateSetting({setting_name: "callbacks_table_filter_options", value: {...filterOptions, [selectedColumn.key]: value }});
|
||||
}catch(error){
|
||||
console.log("failed to save filter options");
|
||||
}
|
||||
@@ -896,6 +998,13 @@ function CallbacksTablePreMemo(props){
|
||||
const onResetColumnReorder = () => {
|
||||
onSubmitColumnReorder(callbackTableInitialColumns);
|
||||
}
|
||||
const onSubmitSelectMultiple = (callbacks) => {
|
||||
eventingDataRef.current = {
|
||||
trigger_context_type: "callback_id",
|
||||
trigger_context_ids: callbacks.map(c => c.id)
|
||||
};
|
||||
setOpenEventingDialog(true);
|
||||
}
|
||||
return (
|
||||
<div style={{width: '100%', height: '100%', position: "relative",}}>
|
||||
<MythicResizableGrid
|
||||
@@ -909,16 +1018,19 @@ function CallbacksTablePreMemo(props){
|
||||
onClickHeader={onClickHeader}
|
||||
onDoubleClickRow={onRowDoubleClick}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
onRowContextMenuClick={onRowContextClick}
|
||||
onRowContextMenuClick={onRowContextClick}
|
||||
/>
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xs" open={openContextMenu}
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={<TableFilterDialog
|
||||
selectedColumn={selectedColumn}
|
||||
filterOptions={filterOptions}
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
onClose={()=>{setOpenContextMenu(false);}} />}
|
||||
innerDialog={<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={filterOptions[selectedColumn.key]}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>}
|
||||
/>
|
||||
}
|
||||
{openEditDescriptionDialog &&
|
||||
@@ -1073,6 +1185,31 @@ function CallbacksTablePreMemo(props){
|
||||
}
|
||||
/>
|
||||
}
|
||||
{openEventingDialog &&
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
maxWidth="xl"
|
||||
open={openEventingDialog}
|
||||
onClose={() => {setOpenEventingDialog(false);}}
|
||||
innerDialog={
|
||||
<EventTriggerContextSelectDialog onClose={() => {setOpenEventingDialog(false);}}
|
||||
triggerContext={eventingDataRef.current}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
{openEventingMultipleDialog &&
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
maxWidth="xl"
|
||||
open={openEventingMultipleDialog}
|
||||
onClose={() => {setOpenEventingMultipleDialog(false);}}
|
||||
innerDialog={
|
||||
<CallbacksTabsSelectMultipleDialog onClose={() => {setOpenEventingMultipleDialog(false);}}
|
||||
onSubmit={onSubmitSelectMultiple}/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+4
-1
@@ -48,6 +48,7 @@ export function CallbacksTableEditDescriptionColorDialog(props) {
|
||||
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
|
||||
width="100%"
|
||||
height="100%"
|
||||
fontSize={14}
|
||||
wrapEnabled={props.wrap ? props.wrap : false}
|
||||
minLines={props.maxRows ? props.maxRows : 3}
|
||||
maxLines={props.maxRows ? props.maxRows : 3}
|
||||
@@ -55,7 +56,9 @@ export function CallbacksTableEditDescriptionColorDialog(props) {
|
||||
focus={true}
|
||||
onChange={onChange}
|
||||
setOptions={{
|
||||
useWorker: false
|
||||
tabSize: 4,
|
||||
useWorker: false,
|
||||
showInvisibles: false,
|
||||
}}
|
||||
/>
|
||||
<HexColorPicker color={color} onChange={setColor} style={{width: "100%"}} />
|
||||
|
||||
+13
-3
@@ -17,7 +17,11 @@ import MythicTextField from "../../MythicComponents/MythicTextField";
|
||||
export function CallbacksTableEditTriggerOnCheckinDialog(props) {
|
||||
const [comment, setComment] = React.useState(0);
|
||||
const onChange = (name, value, error) => {
|
||||
setComment(parseInt(value));
|
||||
if(isNaN(parseInt(value))){
|
||||
setComment(0);
|
||||
} else {
|
||||
setComment(parseInt(value));
|
||||
}
|
||||
}
|
||||
useEffect( () => {
|
||||
setComment(props.trigger_on_checkin_after_time);
|
||||
@@ -29,8 +33,14 @@ export function CallbacksTableEditTriggerOnCheckinDialog(props) {
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">{"Adjust this callback's trigger threshold"}</DialogTitle>
|
||||
<DialogContent dividers={true} style={{height: "100%"}}>
|
||||
<Typography>
|
||||
{"This adjusts how long, in minutes, this callback must not checkin before finally checking in to trigger an eventing workflow (trigger is callback_checkin). A zero value means never trigger."}
|
||||
<Typography >
|
||||
This adjusts how long, in minutes, this callback must <b>not</b> checkin before finally checking in to trigger an <b>eventing workflow</b> (trigger is callback_checkin).
|
||||
</Typography>
|
||||
<Typography >
|
||||
A zero value means never trigger.
|
||||
</Typography>
|
||||
<Typography >
|
||||
If no eventing workflow for <b>callback_checkin</b> that matches the right payload_types and supported_os restrictions, then nothing will happen.
|
||||
</Typography>
|
||||
<MythicTextField autoFocus={true} onChange={onChange} type={"number"} value={comment} onEnter={onSubmit} name={"trigger threshold in minutes"} showLabel={false} />
|
||||
</DialogContent>
|
||||
|
||||
@@ -31,6 +31,7 @@ import TerminalIcon from '@mui/icons-material/Terminal';
|
||||
import VerticalSplitIcon from '@mui/icons-material/VerticalSplit';
|
||||
import NotificationsActiveTwoToneIcon from '@mui/icons-material/NotificationsActiveTwoTone';
|
||||
import {faSocks} from '@fortawesome/free-solid-svg-icons';
|
||||
import {TagsDisplay, ViewEditTags} from "../../MythicComponents/MythicTag";
|
||||
|
||||
export const CallbacksTableIDCell = React.memo(({rowData, callbackDropdown}) =>{
|
||||
const theme = useTheme();
|
||||
@@ -248,7 +249,7 @@ export const CallbacksTableIPCell = React.memo(({cellData, rowData}) => {
|
||||
{displayIP}
|
||||
</div>
|
||||
{openPickIP &&
|
||||
<MythicDialog fullWidth={true} open={openPickIP} onClose={() => {setOpenPickIP(false);}}
|
||||
<MythicDialog fullWidth={true} open={openPickIP} onClose={() => {setOpenPickIP(false);}}
|
||||
innerDialog={
|
||||
<MythicSelectFromRawListDialog
|
||||
onClose={() => {setOpenPickIP(false);}}
|
||||
@@ -473,5 +474,13 @@ export const CallbacksTableSleepCell = React.memo( ({rowData, cellData, updateSl
|
||||
</div>
|
||||
|
||||
|
||||
)
|
||||
}, areEqual)
|
||||
export const CallbacksTableTagsCell = React.memo(({rowData, cellData}) => {
|
||||
return (
|
||||
<div>
|
||||
<ViewEditTags target_object={"callback_id"} target_object_id={rowData.id} />
|
||||
<TagsDisplay tags={cellData} expand={false} />
|
||||
</div>
|
||||
)
|
||||
}, areEqual)
|
||||
@@ -10,14 +10,19 @@ import { CallbacksTabsFileBrowserLabel, CallbacksTabsFileBrowserPanel } from './
|
||||
import { CallbacksTabsProcessBrowserLabel, CallbacksTabsProcessBrowserPanel } from './CallbacksTabsProcessBrowser';
|
||||
import { CallbacksTabsTaskingSplitLabel, CallbacksTabsTaskingSplitPanel} from "./CallbacksTabsTaskingSplit";
|
||||
import {CallbacksTabsTaskingConsoleLabel, CallbacksTabsTaskingConsolePanel} from "./CallbacksTabsTaskingConsole";
|
||||
import {
|
||||
CallbacksTabsCustomFileBasedBrowserLabel,
|
||||
CallbacksTabsCustomFileBasedBrowserPanel
|
||||
} from "./CallbacksTabsCustomFileBasedBrowser";
|
||||
|
||||
export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clickedTabId, setClickedTabId, onEditTabDescription, contextMenuOptions, me}) {
|
||||
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);
|
||||
};
|
||||
@@ -31,7 +36,9 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clic
|
||||
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);
|
||||
}
|
||||
@@ -39,11 +46,13 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clic
|
||||
};
|
||||
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]);
|
||||
@@ -66,98 +75,122 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clic
|
||||
|
||||
return (
|
||||
<div style={{width: "100%", maxWidth: "100%", display: 'flex', flexDirection: 'column', flexGrow: 1, height: "100%" }}>
|
||||
<AppBar color='default' position={"static"} >
|
||||
<AppBar color='default' position={"static"} style={{backgroundColor: "transparent"}} >
|
||||
<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}
|
||||
style={{ }}
|
||||
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}
|
||||
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}
|
||||
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}
|
||||
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}
|
||||
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}
|
||||
selectedIndex={value}
|
||||
onDragTab={onDragTab}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</Tabs>
|
||||
<>
|
||||
<Tabs
|
||||
ref={provided.innerRef} {...provided.droppableProps}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
indicatorColor='primary'
|
||||
textColor='primary'
|
||||
variant='fullWidth'
|
||||
scrollButtons={true}
|
||||
style={{ }}
|
||||
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>
|
||||
@@ -177,6 +210,7 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clic
|
||||
key={'tabpanel' + tab.tabID + tab.tabType}
|
||||
onCloseTab={onCloseTabLocal}
|
||||
tabInfo={tab}
|
||||
setNewDataForTab={setNewDataForTab}
|
||||
value={value}
|
||||
index={index}
|
||||
me={me}
|
||||
@@ -196,6 +230,7 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clic
|
||||
key={'tabpanel' + tab.tabID + tab.tabType}
|
||||
onCloseTab={onCloseTabLocal}
|
||||
tabInfo={tab}
|
||||
setNewDataForTab={setNewDataForTab}
|
||||
value={value}
|
||||
index={index}
|
||||
me={me}
|
||||
@@ -214,12 +249,32 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clic
|
||||
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
|
||||
@@ -232,10 +287,12 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clic
|
||||
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':
|
||||
@@ -250,6 +307,7 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clic
|
||||
onCloseTab={onCloseTabLocal}
|
||||
key={'tabpanel' + tab.tabID + tab.tabType}
|
||||
tabInfo={tab}
|
||||
setNewDataForTab={setNewDataForTab}
|
||||
value={value}
|
||||
index={index}
|
||||
me={me}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+976
@@ -0,0 +1,976 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useMutation, useLazyQuery, gql } from '@apollo/client';
|
||||
import { snackActions } from '../../utilities/Snackbar';
|
||||
import {
|
||||
MythicDialog,
|
||||
MythicModifyStringDialog,
|
||||
MythicViewJSONAsTableDialog,
|
||||
} from '../../MythicComponents/MythicDialog';
|
||||
import {faFolder} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import DescriptionIcon from '@mui/icons-material/Description';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import ErrorIcon from '@mui/icons-material/Error';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { IconButton } from '@mui/material';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import ListIcon from '@mui/icons-material/List';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import GetAppIcon from '@mui/icons-material/GetApp';
|
||||
import 'react-virtualized/styles.css';
|
||||
import FileCopyOutlinedIcon from '@mui/icons-material/FileCopyOutlined';
|
||||
import { copyStringToClipboard } from '../../utilities/Clipboard';
|
||||
import MythicResizableGrid from '../../MythicComponents/MythicResizableGrid';
|
||||
import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip';
|
||||
import {faFilter} from '@fortawesome/free-solid-svg-icons';
|
||||
import {MythicTransferListDialog} from '../../MythicComponents/MythicTransferList';
|
||||
import {TagsDisplay, ViewEditTags} from '../../MythicComponents/MythicTag';
|
||||
import { toLocalTime } from '../../utilities/Time';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import {RenderSingleTask} from "../SingleTaskView/SingleTaskView";
|
||||
import {
|
||||
GetComputedFontSize,
|
||||
GetMythicSetting,
|
||||
useSetMythicSetting
|
||||
} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {getIconColor, getIconName} from "./ResponseDisplayTable";
|
||||
import {CallbacksTableColumnsReorderDialog} from "./CallbacksTableColumnsReorderDialog";
|
||||
|
||||
const updateFileComment = gql`
|
||||
mutation updateCommentMutation($mythictree_id: Int!, $comment: String!) {
|
||||
update_mythictree_by_pk(pk_columns: { id: $mythictree_id }, _set: { comment: $comment }) {
|
||||
comment
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CallbacksTabsCustomFileBasedBrowserTable = (props) => {
|
||||
const theme = useTheme();
|
||||
const [updateSetting] = useSetMythicSetting();
|
||||
const [allData, setAllData] = React.useState([]);
|
||||
const [openReorderDialog, setOpenReorderDialog] = React.useState(false);
|
||||
const [openContextMenu, setOpenContextMenu] = React.useState(false);
|
||||
const [filterOptions, setFilterOptions] = React.useState({});
|
||||
const [selectedColumn, setSelectedColumn] = React.useState({});
|
||||
const [fileCommentDialogOpen, setFileCommentDialogOpen] = React.useState(false);
|
||||
const [viewPermissionsDialogOpen, setViewPermissionsDialogOpen] = React.useState(false);
|
||||
const permissionDataRef = React.useRef({metadata: {}, name: ""});
|
||||
const commentDataRef = React.useRef({id: 0, comment: ""});
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [sortData, setSortData] = React.useState({"sortKey": null, "sortDirection": null, "sortType": null})
|
||||
const [columnVisibility, setColumnVisibility] = React.useState({
|
||||
"visible": [...props.treeConfig.table.visible],
|
||||
"hidden": [...props.treeConfig.table.hidden]
|
||||
});
|
||||
const indicatePartialListingRef = React.useRef(props.treeConfig.indicate_partial_listing);
|
||||
const [selectedRows, setSelectedRows] = React.useState([]);
|
||||
const [updateComment] = useMutation(updateFileComment, {
|
||||
onCompleted: (data) => {
|
||||
snackActions.success('updated comment');
|
||||
},
|
||||
});
|
||||
const onSubmitUpdatedComment = (comment) => {
|
||||
updateComment({ variables: { mythictree_id: commentDataRef.current.id, comment: comment } });
|
||||
};
|
||||
const [initialDefaultColumns, setInitialDefaultColumns] = React.useState([
|
||||
{ name: 'name', type: 'name', key: 'name_text', fillWidth: true, visible: true },
|
||||
...props.treeConfig.table.columns,
|
||||
{ name: 'tags', type: 'tags', key: 'tags', disableSort: true, disableFilterMenu: true, width: 220 },
|
||||
{ name: 'comment', type: 'comment', key: 'comment', width: 200 },
|
||||
]);
|
||||
const [columnOrder, setColumnOrder] = React.useState(initialDefaultColumns);
|
||||
const columns = React.useMemo(
|
||||
() =>
|
||||
columnOrder.reduce( (prev, cur) => {
|
||||
if(columnVisibility.visible.includes(cur.name)){
|
||||
if(filterOptions[cur.key] && String(filterOptions[cur.key]).length > 0){
|
||||
return [...prev, {...cur, filtered: true}];
|
||||
}else{
|
||||
return [...prev, {...cur}];
|
||||
}
|
||||
}else{
|
||||
return [...prev];
|
||||
}
|
||||
}, [])
|
||||
, [filterOptions, columnVisibility, columnOrder]
|
||||
);
|
||||
|
||||
const sortedData = React.useMemo(() => {
|
||||
if (sortData.sortKey === null || sortData.sortType === null) {
|
||||
return allData;
|
||||
}
|
||||
const tempData = [...allData];
|
||||
if (sortData.sortType === 'number' || sortData.sortType === 'size' || sortData.sortType === 'date') {
|
||||
tempData.sort((a, b) => {
|
||||
if(sortData.inMetadata){
|
||||
try {
|
||||
if(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a]?.metadata[sortData.sortKey] ===
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b]?.metadata[sortData.sortKey]){
|
||||
return 0;
|
||||
}
|
||||
if (props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a]?.metadata[sortData.sortKey] === null ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a]?.metadata[sortData.sortKey] === undefined) {
|
||||
return -1;
|
||||
}
|
||||
if (props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b]?.metadata[sortData.sortKey] === null ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b]?.metadata[sortData.sortKey] === undefined) {
|
||||
return 1;
|
||||
}
|
||||
return parseInt(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a]?.metadata[sortData.sortKey]) >
|
||||
parseInt(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b]?.metadata[sortData.sortKey]) ? 1 : -1;
|
||||
}catch(error) {
|
||||
console.log("failed to parse data for sorting", error);
|
||||
return props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a]?.metadata[sortData.sortKey] >
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b]?.metadata[sortData.sortKey] ? 1 : -1
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
if(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] ===
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey]){
|
||||
return 0;
|
||||
}
|
||||
if (props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] === null ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] === undefined) {
|
||||
return -1;
|
||||
}
|
||||
if (props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey] === null ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey] === undefined) {
|
||||
return 1;
|
||||
}
|
||||
return parseInt(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey]) > parseInt(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey]) ? 1 : -1;
|
||||
} catch (error) {
|
||||
console.log("failed to parse data for sorting", error);
|
||||
return props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] > props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
} else if (sortData.sortType === 'string') {
|
||||
tempData.sort((a, b) => {
|
||||
try{
|
||||
if(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] ===
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey]){
|
||||
return 0;
|
||||
}
|
||||
if (props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] === null ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] === undefined) {
|
||||
return -1;
|
||||
}
|
||||
if (props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey] === null ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey] === undefined) {
|
||||
return 1;
|
||||
}
|
||||
return props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey].localeCompare(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey]);
|
||||
}catch(error){
|
||||
console.log("failed to parse data for sorting", error);
|
||||
return props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] > props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey] ? 1 : -1
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
if (sortData.sortDirection === 'DESC') {
|
||||
tempData.reverse();
|
||||
}
|
||||
return tempData;
|
||||
}, [allData, sortData]);
|
||||
const onSubmitFilterOptions = (value) => {
|
||||
setFilterOptions({...filterOptions, [selectedColumn.key]: value });
|
||||
try{
|
||||
updateSetting({setting_name: `${props.treeConfig.name}_browser_filter_options`, value: {...filterOptions, [selectedColumn.key]: value }});
|
||||
}catch(error){
|
||||
console.log("failed to save filter options");
|
||||
}
|
||||
}
|
||||
const filterRow = (row) => {
|
||||
if(props.treeRootData[props.selectedFolderData.group] === undefined ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host] === undefined ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row] === undefined) {
|
||||
return true;
|
||||
}
|
||||
if(!props.showDeletedFiles && props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.deleted){
|
||||
return true;
|
||||
}
|
||||
for(const [key,value] of Object.entries(filterOptions)){
|
||||
if(["name_text", "comment"].includes(key)){
|
||||
if(!String(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row][key]).toLowerCase().includes(value)){
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if(!String(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row].metadata[key]).toLowerCase().includes(value)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const gridData = React.useMemo(
|
||||
() =>
|
||||
// row is just the name
|
||||
sortedData.reduce((prev, row) => {
|
||||
if(filterRow(row)){
|
||||
return [...prev];
|
||||
}else{
|
||||
return [...prev, columns.map( c => {
|
||||
switch(c.type) {
|
||||
case "name":
|
||||
return <FileBrowserTableRowNameCell
|
||||
treeRootData={props.treeRootData[props.selectedFolderData.group]}
|
||||
selectedFolderData={props.selectedFolderData}
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row].name_text}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}/>;
|
||||
case "tags":
|
||||
return <FileBrowserTagsCell
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}
|
||||
treeRootData={props.treeRootData[props.selectedFolderData.group]}
|
||||
cellData={row}
|
||||
selectedFolderData={props.selectedFolderData}
|
||||
me={props.me}/>
|
||||
case "comment":
|
||||
return <FileBrowserTableRowStringCell
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row].comment}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}/>
|
||||
case "date":
|
||||
return <TableRowDateCell
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.metadata?.[c.key] || null}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}
|
||||
view_utc_time={props.me?.user?.view_utc_time}/>
|
||||
case "size":
|
||||
return <TableRowSizeCell
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.metadata?.[c.key] || 0}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}/>
|
||||
case "string":
|
||||
return <FileBrowserTableRowStringCell
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.metadata?.[c.key] || ""}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}/>
|
||||
default:
|
||||
return <FileBrowserTableRowStringCell
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.metadata?.[c.key] || ""}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}/>
|
||||
}
|
||||
})];
|
||||
}
|
||||
|
||||
}, []),
|
||||
[sortedData, props.onTaskRowAction, filterOptions, columnVisibility, props.showDeletedFiles]
|
||||
);
|
||||
const getDisplayFormat = () => {
|
||||
if(sortedData.length === 0 && props?.selectedFolderData?.success === false){
|
||||
return "showTask";
|
||||
}else if(sortedData.length > 0 || props?.selectedFolderData?.success){
|
||||
return "normal";
|
||||
} else if(sortedData.length === 0 && props?.selectedFolderData?.has_children && props?.selectedFolderData?.success === null){
|
||||
return "fetchLocal";
|
||||
}else if(sortedData.length === 0 && !props?.selectedFolderData?.has_children && props?.selectedFolderData?.success === null) {
|
||||
return "fetchRemote";
|
||||
}else if(sortedData.length === 0){
|
||||
return "fetchRemote";
|
||||
} else {
|
||||
return "normal";
|
||||
}
|
||||
}
|
||||
const displayFormat = getDisplayFormat();
|
||||
useEffect(() => {
|
||||
// when the folder changes, we need to aggregate all of the entries
|
||||
//console.log(props.selectedFolderData, props.treeAdjMatrix, props.treeRootData)
|
||||
let desiredPath = props.selectedFolderData.full_path_text;
|
||||
if(props.selectedFolderData.id === props.selectedFolderData.host){
|
||||
desiredPath = "";
|
||||
}
|
||||
let newAllData = Object.keys(props.treeAdjMatrix[props.selectedFolderData.group]?.[props.selectedFolderData.host]?.[desiredPath] || {});
|
||||
setAllData(newAllData);
|
||||
//console.log("just set all data")
|
||||
}, [props.selectedFolderData, props.treeAdjMatrix]);
|
||||
const onRowDoubleClick = (e, rowIndex, rowData) => {
|
||||
if (!rowData.can_have_children) {
|
||||
return;
|
||||
}
|
||||
snackActions.info('Fetching contents from database...');
|
||||
props.onRowDoubleClick({...rowData, group: props.selectedFolderData.group});
|
||||
setSortData({"sortKey": null, "sortType":null, "sortDirection": "ASC"});
|
||||
};
|
||||
const onClickHeader = (e, columnIndex) => {
|
||||
const column = columns[columnIndex];
|
||||
if(column.disableSort){
|
||||
return;
|
||||
}
|
||||
if (!column.key) {
|
||||
setSortData({"sortKey": null, "sortType":null, "sortDirection": "ASC", "inMetadata": false});
|
||||
}
|
||||
if (sortData.sortKey === column.key) {
|
||||
if (sortData.sortDirection === 'ASC') {
|
||||
setSortData({...sortData, "sortDirection": "DESC"});
|
||||
} else {
|
||||
setSortData({"sortKey": null, "sortType":null, "sortDirection": "ASC", "inMetadata": false});
|
||||
}
|
||||
} else {
|
||||
setSortData({"sortKey": column.key, "inMetadata": column.inMetadata, "sortType":column.type, "sortDirection": "ASC"});
|
||||
}
|
||||
};
|
||||
const contextMenuOptions = [
|
||||
{
|
||||
name: 'Filter Column', type: "item",
|
||||
icon: <FontAwesomeIcon icon={faFilter} style={{paddingRight: "5px"}} />,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
if(columns[columnIndex].disableFilterMenu){
|
||||
snackActions.warning("Can't filter that column");
|
||||
return;
|
||||
}
|
||||
setSelectedColumn(columns[columnIndex]);
|
||||
setOpenContextMenu(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Reorder Columns and Adjust Visibility", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
setOpenReorderDialog(true);
|
||||
}
|
||||
}
|
||||
];
|
||||
const sortColumn = columns.findIndex((column) => column.key === sortData.sortKey);
|
||||
const onRowClick = ({event, rowDataStatic}) => {
|
||||
if(event.ctrlKey || event.metaKey){
|
||||
if(props.treeRootData?.[props.selectedFolderData.group]?.[rowDataStatic.host]?.[rowDataStatic.full_path_text]){
|
||||
props.treeRootData[props.selectedFolderData.group][rowDataStatic.host][rowDataStatic.full_path_text].selected = true;
|
||||
}
|
||||
|
||||
setSelectedRows([...selectedRows, rowDataStatic]);
|
||||
} else {
|
||||
if(props.treeRootData?.[props.selectedFolderData.group]?.[rowDataStatic.host]?.[rowDataStatic.full_path_text]){
|
||||
props.treeRootData[props.selectedFolderData.group][rowDataStatic.host][rowDataStatic.full_path_text].selected = true;
|
||||
}
|
||||
for(let i = 0; i < selectedRows.length; i++){
|
||||
if(props.treeRootData?.[props.selectedFolderData.group]?.[selectedRows[i].host]?.[selectedRows[i].full_path_text]){
|
||||
props.treeRootData[props.selectedFolderData.group][selectedRows[i].host][selectedRows[i].full_path_text].selected = false;
|
||||
}
|
||||
}
|
||||
setSelectedRows([rowDataStatic]);
|
||||
}
|
||||
}
|
||||
const optionsA = (element) => [
|
||||
{
|
||||
name: element?.name_text, icon: null, click: ({event}) => {},
|
||||
type: "item", disabled: true
|
||||
},
|
||||
{
|
||||
name: "Copy to Clipboard", icon: null, click: () => {}, type: "menu",
|
||||
menuItems: [
|
||||
{
|
||||
name: 'Name', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(element.name_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Full Path', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(element.full_path_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Metadata', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(JSON.stringify(element?.metadata, null, 2))){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'View Metadata', type: "item",
|
||||
icon: <VisibilityIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
permissionDataRef.current.metadata = element?.metadata;
|
||||
permissionDataRef.current.name = element.name_text;
|
||||
setViewPermissionsDialogOpen(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Edit Comment', type: "item",
|
||||
icon: <EditIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
commentDataRef.current.id = element.id;
|
||||
commentDataRef.current.comment = element.comment;
|
||||
setFileCommentDialogOpen(true);
|
||||
},
|
||||
},
|
||||
]
|
||||
async function optionsB (callback_id, callback_display_id, element) {
|
||||
let downloadCommand = await props.getLoadedCommandForUIFeature(callback_id, element.can_have_children ? props.baseUIFeature + ":download_folder" : props.baseUIFeature + ":download");
|
||||
let downloadDisplay = "Download (Unsupported)";
|
||||
if(element.can_have_children){
|
||||
downloadDisplay = "Download Folder (Unsupported)";
|
||||
}
|
||||
if(downloadCommand !== undefined){
|
||||
downloadDisplay = `Download (${downloadCommand.command.cmd})`;
|
||||
if(element.can_have_children){
|
||||
downloadDisplay = `Download Folder (${downloadCommand.command.cmd})`;
|
||||
}
|
||||
}
|
||||
let listCommand = await props.getLoadedCommandForUIFeature(callback_id, props.baseUIFeature + ":list");
|
||||
let listDisplay = "List (Unsupported)";
|
||||
if(listCommand !== undefined){
|
||||
listDisplay = `List (${listCommand.command.cmd})`;
|
||||
}
|
||||
let removeCommand = await props.getLoadedCommandForUIFeature(callback_id, props.baseUIFeature + ":remove");
|
||||
let removeDisplay = "Remove (Unsupported)";
|
||||
if(removeCommand !== undefined){
|
||||
removeDisplay = `Remove (${removeCommand.command.cmd})`;
|
||||
}
|
||||
return [
|
||||
{
|
||||
name: downloadDisplay, type: "item",
|
||||
disabled: downloadCommand === undefined,
|
||||
icon: <GetAppIcon color="success" style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
props.onTaskRowAction({
|
||||
path: element.parent_path_text,
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: element.can_have_children ? props.baseUIFeature + ':download_folder' : props.baseUIFeature + ':download',
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: listDisplay, type: "item",
|
||||
disabled: listCommand === undefined,
|
||||
icon: <ListIcon color="warning" style={{ paddingRight: '5px'}} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
props.onTaskRowAction({
|
||||
path: element.parent_path_text,
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: props.baseUIFeature + ':list',
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: removeDisplay, type: "item",
|
||||
disabled: removeCommand === undefined,
|
||||
icon: <DeleteIcon color="error" style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
props.onTaskRowAction({
|
||||
path: element.parent_path_text,
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: props.baseUIFeature + ':remove',
|
||||
getConfirmation: true,
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
async function optionsC (callback_id, callback_display_id, element, action_name, ui_feature, icon, color, openDialog, getConfirmation) {
|
||||
let command = await props.getLoadedCommandForUIFeature(callback_id, ui_feature);
|
||||
let commandDisplay = `${action_name} (Unsupported)`;
|
||||
if(command !== undefined){
|
||||
commandDisplay = `${action_name} (${command.command.cmd})`;
|
||||
}
|
||||
return {
|
||||
name: commandDisplay, type: "item",
|
||||
disabled: command === undefined,
|
||||
icon: <FontAwesomeIcon icon={getIconName(icon)} style={{marginRight: "5px", color: getIconColor(theme, color || "")}}/>,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
props.onTaskRowAction({
|
||||
path: element.parent_path_text,
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: ui_feature, openDialog, getConfirmation,
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
async function onRowContextClick({rowDataStatic}) {
|
||||
// based on row, return updated options array?
|
||||
if(selectedRows.length > 1){
|
||||
let downloadCommand = await props.getLoadedCommandForUIFeature(rowDataStatic.callback.id, rowDataStatic.can_have_children ? props.baseUIFeature + ":download_folder" : props.baseUIFeature + ":download");
|
||||
let listCommand = await props.getLoadedCommandForUIFeature(rowDataStatic.callback.id, props.baseUIFeature + ":list");
|
||||
let removeCommand = await props.getLoadedCommandForUIFeature(rowDataStatic.callback.id, props.baseUIFeature + ":remove");
|
||||
return [
|
||||
{
|
||||
name: `Download All Selected`, type: "item",
|
||||
disabled: downloadCommand === undefined,
|
||||
icon: <GetAppIcon color="success" style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
let newTasks = [];
|
||||
for(let i = 0; i < selectedRows.length; i++){
|
||||
newTasks.push({
|
||||
path: selectedRows[i].parent_path_text,
|
||||
full_path: selectedRows[i].full_path_text,
|
||||
host: selectedRows[i].host,
|
||||
filename: selectedRows[i].name_text,
|
||||
uifeature: selectedRows[i].can_have_children ? props.baseUIFeature + ':download_folder': props.baseUIFeature + ':download',
|
||||
callback_id: props.tabInfo.callbackID,
|
||||
callback_display_id: props.tabInfo.displayID
|
||||
});
|
||||
}
|
||||
props.onTaskRowActions(newTasks);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `Remove All Selected`, type: "item",
|
||||
disabled: removeCommand === undefined,
|
||||
icon: <DeleteIcon color="error" style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
let newTasks = [];
|
||||
for(let i = 0; i < selectedRows.length; i++){
|
||||
newTasks.push({
|
||||
path: selectedRows[i].parent_path_text,
|
||||
full_path: selectedRows[i].full_path_text,
|
||||
host: selectedRows[i].host,
|
||||
filename: selectedRows[i].name_text,
|
||||
uifeature: props.baseUIFeature + ':remove',
|
||||
getConfirmation: true,
|
||||
callback_id: props.tabInfo.callbackID,
|
||||
callback_display_id: props.tabInfo.displayID
|
||||
});
|
||||
}
|
||||
props.onTaskRowActions(newTasks);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `List All Selected`, type: "item",
|
||||
disabled: listCommand === undefined,
|
||||
icon: <ListIcon color="warning" style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
let newTasks = [];
|
||||
for(let i = 0; i < selectedRows.length; i++){
|
||||
newTasks.push({
|
||||
path: selectedRows[i].parent_path_text,
|
||||
full_path: selectedRows[i].full_path_text,
|
||||
host: selectedRows[i].host,
|
||||
filename: selectedRows[i].name_text,
|
||||
uifeature: props.baseUIFeature + ':list',
|
||||
callback_id: props.tabInfo.callbackID,
|
||||
callback_display_id: props.tabInfo.displayID
|
||||
});
|
||||
}
|
||||
props.onTaskRowActions(newTasks);
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
let options = [...optionsA(rowDataStatic)];
|
||||
options.push(...(await optionsB(props.tabInfo.callbackID, props.tabInfo.displayID, rowDataStatic)));
|
||||
if(props.treeConfig.row_actions.length > 0){
|
||||
let extraOptions = [];
|
||||
for(let i = 0; i < props.treeConfig.row_actions.length; i++){
|
||||
let r = props.treeConfig.row_actions[i];
|
||||
if((r.supports_file && !rowDataStatic.can_have_children) || (r.supports_folder && rowDataStatic.can_have_children) ){
|
||||
extraOptions.push(await optionsC(rowDataStatic.callback.id, rowDataStatic.callback.display_id,
|
||||
rowDataStatic, r.name, r.ui_feature, r.icon, r.color, r.openDialog, r.getConfirmation));
|
||||
}
|
||||
}
|
||||
if(extraOptions.length > 0){
|
||||
options.push({
|
||||
name: `Custom Actions`, icon: null, click: () => {}, type: "menu",
|
||||
menuItems: extraOptions
|
||||
})
|
||||
}
|
||||
}
|
||||
if(rowDataStatic.callback.display_id !== props.tabInfo.displayID){
|
||||
options.push({
|
||||
name: `Original Callback: ${rowDataStatic.callback.display_id}`, icon: null, click: () => {}, type: "menu",
|
||||
menuItems: [
|
||||
...(await optionsB(rowDataStatic.callback.id, rowDataStatic.callback.display_id, rowDataStatic))
|
||||
]
|
||||
})
|
||||
}
|
||||
return options;
|
||||
}
|
||||
React.useEffect( () => {
|
||||
// on startup, want to see if `callbacks_table_columns` exists in storage and load it if possible
|
||||
try {
|
||||
const storageItem = GetMythicSetting({setting_name: `${props.treeConfig.name}_browser_table_columns`, default_value: [...props.treeConfig.table.visible]});
|
||||
if(storageItem !== null){
|
||||
let allColumns = [...columnVisibility["visible"].map(c => c), ...columnVisibility["hidden"].map(c => c)];
|
||||
let newHidden = [];
|
||||
allColumns.forEach((v,i,a) => {
|
||||
if(!storageItem.includes(v)){
|
||||
newHidden.push(v);
|
||||
}
|
||||
});
|
||||
if(storageItem.length !== 0){
|
||||
setColumnVisibility({visible: storageItem, hidden: newHidden});
|
||||
}
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_columns", error);
|
||||
}
|
||||
try {
|
||||
const storageItemOptions = GetMythicSetting({setting_name: `${props.treeConfig.name}_browser_filter_options`, default_value: {}});
|
||||
if(storageItemOptions !== null){
|
||||
setFilterOptions(storageItemOptions);
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_filter_options", error);
|
||||
}
|
||||
try {
|
||||
const storageColumnOrder = GetMythicSetting({setting_name: `${props.treeConfig.name}_browser_column_order`, default_value: columns.map(c => c.name)});
|
||||
if(storageColumnOrder !== null){
|
||||
let newOrder = [];
|
||||
for(let i = 0; i < storageColumnOrder.length; i++){
|
||||
for(let j = 0; j < columnOrder.length; j++){
|
||||
if(columnOrder[j].name === storageColumnOrder[i]){
|
||||
newOrder.push(columnOrder[j]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(newOrder.length === columns.length){
|
||||
setColumnOrder(newOrder);
|
||||
}
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_filter_options", error);
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
const onSubmitColumnReorder = (newOrder) => {
|
||||
let newVisible = [];
|
||||
let newHidden = [];
|
||||
for(let i = 0; i < newOrder.length; i++){
|
||||
if(newOrder[i].visible){
|
||||
newVisible.push(newOrder[i].name);
|
||||
} else {
|
||||
newHidden.push(newOrder[i].name);
|
||||
}
|
||||
}
|
||||
if(newVisible.length === 0){
|
||||
snackActions.error("Can't update to show no fields");
|
||||
return;
|
||||
}
|
||||
updateSetting({setting_name: `${props.treeConfig.name}_browser_column_order`, value: newOrder.map(c => c.name)});
|
||||
setColumnOrder(newOrder);
|
||||
setColumnVisibility({visible: newVisible, hidden: newHidden});
|
||||
updateSetting({setting_name: `${props.treeConfig.name}_browser_table_columns`, value: newVisible});
|
||||
setOpenReorderDialog(false);
|
||||
}
|
||||
const onResetColumnReorder = () => {
|
||||
onSubmitColumnReorder(initialDefaultColumns);
|
||||
}
|
||||
if(loading){
|
||||
return (
|
||||
<div style={{width: '100%', height: '100%', position: "relative",}}>
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%"
|
||||
}}>
|
||||
{"Loading Saved Browser Customizations..."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div style={{width: '100%', height: '100%', overflow: "hidden", position: "relative"}}>
|
||||
{displayFormat === "normal" &&
|
||||
<>
|
||||
<MythicResizableGrid
|
||||
columns={columns}
|
||||
sortIndicatorIndex={sortColumn}
|
||||
sortDirection={sortData.sortDirection}
|
||||
items={gridData}
|
||||
rowHeight={GetComputedFontSize() + 7}
|
||||
onClickHeader={onClickHeader}
|
||||
onDoubleClickRow={onRowDoubleClick}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
onRowContextMenuClick={onRowContextClick}
|
||||
onRowClick={onRowClick}
|
||||
/>
|
||||
{fileCommentDialogOpen && (
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
maxWidth='md'
|
||||
open={fileCommentDialogOpen}
|
||||
onClose={() => {
|
||||
setFileCommentDialogOpen(false);
|
||||
}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Edit Comment'
|
||||
onSubmit={onSubmitUpdatedComment}
|
||||
value={commentDataRef.current?.value}
|
||||
onClose={() => {
|
||||
setFileCommentDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{viewPermissionsDialogOpen && (
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
maxWidth='lg'
|
||||
open={viewPermissionsDialogOpen}
|
||||
onClose={() => {
|
||||
setViewPermissionsDialogOpen(false);
|
||||
}}
|
||||
innerDialog={
|
||||
<MythicViewJSONAsTableDialog
|
||||
title={'View Metadata for ' + permissionDataRef.current?.name}
|
||||
leftColumn='Attribute'
|
||||
rightColumn='Value'
|
||||
value={permissionDataRef.current?.metadata}
|
||||
onClose={() => {
|
||||
setViewPermissionsDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{openReorderDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openReorderDialog}
|
||||
onClose={()=>{setOpenReorderDialog(false);}}
|
||||
innerDialog={
|
||||
<CallbacksTableColumnsReorderDialog
|
||||
onClose={()=>{setOpenReorderDialog(false);}}
|
||||
visible={columnVisibility.visible}
|
||||
hidden={columnVisibility.hidden}
|
||||
onReset={onResetColumnReorder}
|
||||
onSubmit={onSubmitColumnReorder}
|
||||
initialItems={columnOrder}
|
||||
/>}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
}
|
||||
{displayFormat === "normal" && props?.selectedFolderData?.success === null && indicatePartialListingRef.current &&
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid black",
|
||||
padding: "5px",
|
||||
backgroundColor: "rgba(37,37,37,0.92)", color: "white",
|
||||
}}>
|
||||
{"Only PARTIAL data has been collected for this path. "}<br/>
|
||||
{"Task this callback to list the contents"}
|
||||
<IconButton style={{margin: 0, padding: 0, marginRight: "10px"}}
|
||||
onClick={props.onListFilesButtonFromTableWithNoEntries}>
|
||||
<RefreshIcon color={"info"} fontSize={"large"}
|
||||
style={{display: "inline-block",}}/>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{displayFormat === "fetchLocal" && indicatePartialListingRef.current &&
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid black",
|
||||
padding: "5px",
|
||||
backgroundColor: "rgba(37,37,37,0.92)", color: "white",
|
||||
}}>
|
||||
{"Some data exists for this path, but isn't loaded into the UI. "}
|
||||
<br/>
|
||||
{"Click the folder icon to fetch data from the database."}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{displayFormat === "fetchRemote" && indicatePartialListingRef.current &&
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid black",
|
||||
padding: "5px",
|
||||
backgroundColor: "rgba(37,37,37,0.92)", color: "white",
|
||||
}}>
|
||||
{"No data has been collected for this path. "}
|
||||
<div style={{display: "flex", alignItems: "center"}}>
|
||||
{"Task this callback to list the contents"}
|
||||
<IconButton style={{margin: 0, padding: 0, marginRight: "10px"}} onClick={props.onListFilesButtonFromTableWithNoEntries} >
|
||||
<RefreshIcon color={"info"} fontSize={"large"}
|
||||
style={{ display: "inline-block",}} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{displayFormat === "showTask" && props.selectedFolderData?.task_id > 0 &&
|
||||
<RenderSingleTask task_id={props.selectedFolderData?.task_id} />
|
||||
}
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openContextMenu}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={filterOptions[selectedColumn.key]}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const FileBrowserTableRowNameCell = ({cellData, rowData, treeRootData, selectedFolderData }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div style={{ alignItems: 'center', display: 'flex', maxHeight: "100%", textDecoration: treeRootData[selectedFolderData.host][rowData.full_path_text]?.deleted ? 'line-through' : '' }}>
|
||||
{!treeRootData[selectedFolderData.host][rowData.full_path_text]?.can_have_children ? (
|
||||
<DescriptionIcon size={"small"} style={{
|
||||
marginRight: '5px' }} />
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={faFolder}
|
||||
size={"1x"}
|
||||
style={{
|
||||
marginRight: '8px',
|
||||
marginLeft: "4px",
|
||||
color:
|
||||
treeRootData[selectedFolderData.host][rowData.full_path_text]?.success || treeRootData[selectedFolderData.host][rowData.full_path_text]?.has_children
|
||||
? theme.folderColor
|
||||
: theme.emptyFolderColor,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<pre
|
||||
style={{
|
||||
color:
|
||||
treeRootData[selectedFolderData.host][rowData.full_path_text]?.success !== null
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary,
|
||||
}}>
|
||||
{cellData}
|
||||
</pre>
|
||||
{treeRootData[selectedFolderData.host][rowData.full_path_text]?.success === true ? (
|
||||
<MythicStyledTooltip title='Successfully listed contents of folder' tooltipStyle={{display: "inline-flex", marginLeft: "5px"}}>
|
||||
<CheckCircleOutlineIcon color="success" fontSize='small' />
|
||||
</MythicStyledTooltip>
|
||||
) : treeRootData[selectedFolderData.host][rowData.full_path_text]?.success === false ? (
|
||||
<MythicStyledTooltip title='Failed to list contents of folder' tooltipStyle={{display: "inline-flex", marginLeft: "5px"}}>
|
||||
<ErrorIcon fontSize='small' color="error" />
|
||||
</MythicStyledTooltip>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const FileBrowserTagsCell = ({rowData, cellData, treeRootData, selectedFolderData, me}) => {
|
||||
return (
|
||||
<>
|
||||
<ViewEditTags target_object={"mythictree_id"} target_object_id={treeRootData[selectedFolderData.host][cellData]?.id} me={me} />
|
||||
<TagsDisplay tags={treeRootData[selectedFolderData.host][cellData]?.tags} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
const FileBrowserTableRowStringCell = ({ cellData, rowData }) => {
|
||||
//console.log(rowData, cellData)
|
||||
return (
|
||||
<>
|
||||
{cellData}
|
||||
</>
|
||||
)
|
||||
};
|
||||
export const TableRowDateCell = ({ cellData, rowData, view_utc_time }) => {
|
||||
|
||||
try{
|
||||
let cellDataInt = parseInt(cellData)
|
||||
if(cellData === "" || cellData === undefined || cellDataInt <= 0){
|
||||
return "";
|
||||
}
|
||||
let view_utc = true;
|
||||
if(view_utc_time !== undefined){
|
||||
view_utc = view_utc_time
|
||||
}
|
||||
// handle Unix epoch timestamps
|
||||
if (view_utc) {
|
||||
let init_date = new Date(cellDataInt);
|
||||
return init_date.toDateString() + " " + init_date.toTimeString().substring(0, 8) + " UTC";
|
||||
} else {
|
||||
let timezoneDate = new Date(cellDataInt);
|
||||
timezoneDate.setTime(timezoneDate.getTime() - (timezoneDate.getTimezoneOffset() *60*1000));
|
||||
return timezoneDate.toLocaleDateString() + " " + timezoneDate.toLocaleString([], {hour12: true, hour: "2-digit", minute: "2-digit"});
|
||||
}
|
||||
//const dateData = new Date(cellDataInt).toISOString();
|
||||
//return toLocalTime(dateData.slice(0, 10) + " " + dateData.slice(11,-1), view_utc_time);
|
||||
}catch(error){
|
||||
try{
|
||||
let cellDataInt = parseInt(cellData)
|
||||
// handle windows FILETIME values
|
||||
const dateData = new Date( ((cellDataInt / 10000000) - 11644473600) * 1000).toISOString();
|
||||
return toLocalTime(dateData.slice(0, 10) + " " + dateData.slice(11,-1), view_utc_time);
|
||||
}catch(error2){
|
||||
console.log("error with timestamp: ", cellData);
|
||||
return String(cellData);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
export const TableRowSizeCell = ({ cellData, rowData }) => {
|
||||
const getStringSize = () => {
|
||||
try {
|
||||
// process for getting human readable string from bytes: https://stackoverflow.com/a/18650828
|
||||
let bytes = parseInt(cellData);
|
||||
if (cellData === '' || cellData === undefined) return '';
|
||||
if (bytes === 0) return '0 B';
|
||||
const decimals = 2;
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
} catch (error) {
|
||||
return cellData;
|
||||
}
|
||||
};
|
||||
return getStringSize(cellData);
|
||||
};
|
||||
@@ -47,10 +47,12 @@ const fileDataFragment = gql`
|
||||
color
|
||||
id
|
||||
}
|
||||
id
|
||||
}
|
||||
host
|
||||
id
|
||||
can_have_children
|
||||
has_children
|
||||
success
|
||||
full_path_text
|
||||
name_text
|
||||
@@ -68,7 +70,7 @@ const fileDataFragment = gql`
|
||||
const rootFileQuery = gql`
|
||||
${fileDataFragment}
|
||||
query myRootFolderQuery {
|
||||
mythictree(where: { parent_path_text: { _eq: "" }, tree_type: {_eq: "file"} }) {
|
||||
mythictree(where: { parent_path_text: { _eq: "" }, tree_type: {_eq: "file"} }, order_by: {id: desc}) {
|
||||
...fileObjData
|
||||
}
|
||||
}
|
||||
@@ -173,7 +175,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}
|
||||
@@ -196,7 +198,7 @@ export function CallbacksTabsFileBrowserLabel(props) {
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) => {
|
||||
export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNewDataForTab, baseUIFeature }) => {
|
||||
const fromNow = React.useRef(getSkewedNow());
|
||||
const [backdropOpen, setBackdropOpen] = React.useState(false);
|
||||
const treeRootDataRef = React.useRef({}); // hold all of the actual data
|
||||
@@ -215,7 +217,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
|
||||
token: 0,
|
||||
});
|
||||
const autoTaskLsOnEmptyDirectoriesRef = React.useRef(false);
|
||||
const taskingData = React.useRef({"parameters": "", "ui_feature": "file_browser:list"});
|
||||
const taskingData = React.useRef({"parameters": "", "ui_feature": baseUIFeature +":list"});
|
||||
const mountedRef = React.useRef(true);
|
||||
const tableOpenedPathIdRef = React.useRef({
|
||||
host: "",
|
||||
@@ -255,6 +257,10 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
|
||||
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] = {...data.mythictree[i]}
|
||||
}
|
||||
}
|
||||
if(data.mythictree[i].id > treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]].id){
|
||||
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] |= data.mythictree[i].has_children;
|
||||
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]].success |= data.mythictree[i].success;
|
||||
}
|
||||
}
|
||||
}
|
||||
// create the top level data in the adjacency matrix
|
||||
@@ -315,19 +321,29 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
|
||||
} else {
|
||||
// we need to merge data in because we already have some info
|
||||
let existingData = treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]];
|
||||
if(existingData.task_id < data.data.mythictree_stream[i].task_id){
|
||||
if(existingData.task_id <= data.data.mythictree_stream[i].task_id){
|
||||
existingData.deleted = data.data.mythictree_stream[i].deleted;
|
||||
}
|
||||
if( (existingData.success === null || !existingData.success) && data.data.mythictree_stream[i].success !== null){
|
||||
existingData.success = data.data.mythictree_stream[i].success;
|
||||
existingData.task_id = data.data.mythictree_stream[i].task_id;
|
||||
}
|
||||
existingData.comment += data.data.mythictree_stream[i].comment;
|
||||
existingData.comment = data.data.mythictree_stream[i].comment;
|
||||
existingData.tags = [...existingData.tags, ...data.data.mythictree_stream[i].tags];
|
||||
if(!existingData.metadata.has_children){
|
||||
existingData.metadata.has_children = data.data.mythictree_stream[i].metadata.has_children;
|
||||
existingData.has_children = data.data.mythictree_stream[i].has_children || existingData.has_children;
|
||||
existingData.metadata.size = data.data.mythictree_stream[i].metadata.size;
|
||||
existingData.metadata.access_time = data.data.mythictree_stream[i].metadata.access_time;
|
||||
existingData.metadata.modify_time = data.data.mythictree_stream[i].metadata.modify_time;
|
||||
if(data.data.mythictree_stream[i].metadata.permissions !== undefined && data.data.mythictree_stream[i].metadata.permissions !== null){
|
||||
if(Array.isArray(existingData.metadata.permissions) && Array.isArray(data.data.mythictree_stream[i].metadata.permissions)){
|
||||
existingData.metadata.permissions = [...existingData.metadata.permissions, ...data.data.mythictree_stream[i].metadata.permissions];
|
||||
} else if(Array.isArray(existingData.metadata.permissions) !== Array.isArray(data.data.mythictree_stream[i].metadata.permissions)) {
|
||||
// had array/dict and got mismatched array/dict, so just take newer one
|
||||
existingData.metadata.permissions = data.data.mythictree_stream[i].metadata.permissions;
|
||||
} else {
|
||||
existingData.metadata.permissions = {...existingData.metadata.permissions, ...data.data.mythictree_stream[i].metadata.permissions};
|
||||
}
|
||||
}
|
||||
existingData.metadata.permissions = {...existingData.metadata.permissions, ...data.data.mythictree_stream[i].metadata.permissions};
|
||||
let newfileData = data.data.mythictree_stream[i].filemeta.map(f => {
|
||||
return {...f, filename_text: b64DecodeUnicode(f.filename_text)};
|
||||
})
|
||||
@@ -360,7 +376,9 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
|
||||
return prev;
|
||||
}, {...treeAdjMtx});
|
||||
setTreeAdjMtx(newMatrix);
|
||||
|
||||
if(index !== value){
|
||||
setNewDataForTab((prev) => {return {...prev, [tabInfo.tabID]: true}});
|
||||
}
|
||||
}
|
||||
})
|
||||
useSubscription(fileBrowserTaskSub, {
|
||||
@@ -369,7 +387,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
|
||||
onData: ({data}) => {
|
||||
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.warning(<RenderSingleTask task_id={data.data.task_stream[i].id} />,
|
||||
snackActions.error(<RenderSingleTask task_id={data.data.task_stream[i].id} />,
|
||||
{toastId: data.data.task_stream[i].id, autoClose: false, closeOnClick: false});
|
||||
}
|
||||
}
|
||||
@@ -444,7 +462,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
|
||||
} else {
|
||||
// we need to merge data in because we already have some info
|
||||
let existingData = treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]][mythictree[i]["full_path_text"]];
|
||||
if(existingData.task_id < mythictree[i].task_id){
|
||||
if(existingData.task_id <= mythictree[i].task_id){
|
||||
existingData.deleted = mythictree[i].deleted;
|
||||
}
|
||||
if( (existingData.success === null || !existingData.success) && mythictree[i].success !== null){
|
||||
@@ -453,10 +471,21 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
|
||||
}
|
||||
existingData.comment += mythictree[i].comment;
|
||||
existingData.tags = [...existingData.tags, ...mythictree[i].tags];
|
||||
if(!existingData.metadata.has_children){
|
||||
existingData.metadata.has_children = mythictree[i].metadata.has_children;
|
||||
existingData.has_children = mythictree[i].has_children || existingData.has_children;
|
||||
existingData.metadata.size = mythictree[i].metadata.size;
|
||||
existingData.metadata.access_time = mythictree[i].metadata.access_time;
|
||||
existingData.metadata.modify_time = mythictree[i].metadata.modify_time;
|
||||
//console.log("updating permissions", "existing", existingData.metadata.permissions, "new", mythictree[i].metadata)
|
||||
if(mythictree[i].metadata.permissions !== undefined && mythictree[i].metadata.permissions !== null){
|
||||
if(Array.isArray(existingData.metadata.permissions) && Array.isArray(mythictree[i].metadata.permissions)){
|
||||
existingData.metadata.permissions = [...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};
|
||||
}
|
||||
}
|
||||
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)};
|
||||
})
|
||||
@@ -554,7 +583,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
|
||||
taskingData.current = ({
|
||||
"token": token,
|
||||
"parameters": {path: path, full_path: path, host: selectedFolderData.host, file: ""},
|
||||
"ui_feature": "file_browser:list", callback_id, callback_display_id});
|
||||
"ui_feature": baseUIFeature + ":list", callback_id, callback_display_id});
|
||||
setOpenTaskingButton(true);
|
||||
};
|
||||
const onListFilesButtonFromTableWithNoEntries = () => {
|
||||
@@ -566,7 +595,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
|
||||
host: selectedFolderData.host,
|
||||
file: ""
|
||||
},
|
||||
"ui_feature": "file_browser:list",
|
||||
"ui_feature": baseUIFeature +":list",
|
||||
callback_id: tabInfo.callbackID,
|
||||
callback_display_id: tabInfo.displayID});
|
||||
setOpenTaskingButton(true);
|
||||
@@ -575,7 +604,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
|
||||
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);
|
||||
@@ -635,7 +664,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
|
||||
taskingData.current = ({
|
||||
"token": localSelectedToken.current,
|
||||
"parameters": {path: nodeData.full_path_text, full_path: nodeData.full_path_text, host: nodeData.host, file: ""},
|
||||
"ui_feature": "file_browser:list", callback_id, callback_display_id});
|
||||
"ui_feature": baseUIFeature + ":list", callback_id, callback_display_id});
|
||||
setOpenTaskingButton(true);
|
||||
}
|
||||
const openDirectoryPath = ({group, host, path}) => {
|
||||
@@ -669,6 +698,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
|
||||
taskListing={taskListing}
|
||||
tableOpenedPathId={tableOpenedPathIdRef.current}
|
||||
getLoadedCommandForUIFeature={getLoadedCommandForUIFeature}
|
||||
baseUIFeature={baseUIFeature}
|
||||
/>
|
||||
|
||||
</div>
|
||||
@@ -687,6 +717,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
|
||||
fetchParentFolderData={fetchParentFolderData}
|
||||
fetchFolderData={fetchFolderData}
|
||||
openDirectoryPath={openDirectoryPath}
|
||||
baseUIFeature={baseUIFeature}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flexGrow: 1 }}>
|
||||
@@ -706,6 +737,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
|
||||
onTaskRowActions={onTaskRowActions}
|
||||
getLoadedCommandForUIFeature={getLoadedCommandForUIFeature}
|
||||
me={me}
|
||||
baseUIFeature={baseUIFeature}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -737,7 +769,8 @@ const FileBrowserTableTop = ({
|
||||
fetchParentFolderData,
|
||||
fetchFolderData,
|
||||
openDirectoryPath,
|
||||
tabInfo
|
||||
tabInfo,
|
||||
baseUIFeature
|
||||
}) => {
|
||||
const autoTaskLsOnEmptyDirectories = useGetMythicSetting({
|
||||
setting_name: "autoTaskLsOnEmptyDirectories", default_value: false
|
||||
@@ -773,6 +806,9 @@ const FileBrowserTableTop = ({
|
||||
shouldResubscribe: true,
|
||||
onData: ({data}) => {
|
||||
setTokenOptions(data.data.callbacktoken);
|
||||
if(data.data.callbacktoken.length === 0) {
|
||||
onChangeSelectedToken("Default Token");
|
||||
}
|
||||
}
|
||||
});
|
||||
useEffect(() => {
|
||||
@@ -943,9 +979,10 @@ const FileBrowserTableTop = ({
|
||||
borderRight: "1px solid grey",
|
||||
marginRight: "10px",
|
||||
padding: "0 5px 0 0"}}>
|
||||
{tokenOptions.length > 0 ? (
|
||||
<CallbacksTabsTaskingInputTokenSelect options={tokenOptions} changeSelectedToken={changeSelectedToken}/>
|
||||
) : null}
|
||||
{tokenOptions.length > 0 &&
|
||||
<CallbacksTabsTaskingInputTokenSelect width={"100%"}
|
||||
options={tokenOptions} changeSelectedToken={changeSelectedToken}/>
|
||||
}
|
||||
<MythicStyledTooltip title={`Move back to previous listing`}>
|
||||
<IconButton style={{ padding: '3px' }}
|
||||
disabled={historyIndex >= history.length -1 }
|
||||
|
||||
@@ -26,8 +26,6 @@ import FileCopyOutlinedIcon from '@mui/icons-material/FileCopyOutlined';
|
||||
import { copyStringToClipboard } from '../../utilities/Clipboard';
|
||||
import MythicResizableGrid from '../../MythicComponents/MythicResizableGrid';
|
||||
import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip';
|
||||
import {TableFilterDialog} from './TableFilterDialog';
|
||||
import {MythicTransferListDialog} from '../../MythicComponents/MythicTransferList';
|
||||
import {TagsDisplay, ViewEditTags} from '../../MythicComponents/MythicTag';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import { toLocalTime } from '../../utilities/Time';
|
||||
@@ -37,7 +35,12 @@ import {PreviewFileMediaDialog} from "../Search/PreviewFileMedia";
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import {Dropdown, DropdownMenuItem, DropdownNestedMenuItem} from "../../MythicComponents/MythicNestedMenus";
|
||||
import {RenderSingleTask} from "../SingleTaskView/SingleTaskView";
|
||||
import {GetComputedFontSize} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {
|
||||
GetComputedFontSize,
|
||||
GetMythicSetting,
|
||||
useSetMythicSetting
|
||||
} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {CallbacksTableColumnsReorderDialog} from "./CallbacksTableColumnsReorderDialog";
|
||||
|
||||
const getFileDownloadHistory = gql`
|
||||
query getFileDownloadHistory($full_path_text: String!, $host: String!, $group: [String!]) {
|
||||
@@ -76,28 +79,33 @@ const updateFileComment = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
const defaultVisibleColumns = ["Info", "Name", "Size", "Last Modify"];
|
||||
const columnDefaults = [
|
||||
{ name: 'Info', width: 65, disableDoubleClick: true, disableSort: true, disableFilterMenu: true, key: "info", visible: true },
|
||||
{ name: 'Name', type: 'string', key: 'name_text', fillWidth: true, visible: true },
|
||||
{ name: "Size", type: "size", key: "size", inMetadata: true, width: 100, visible: true},
|
||||
{ name: "Last Modify", type: "date", key: "modify_time", inMetadata: true, width: 250, visible: true},
|
||||
{ name: 'Tags', type: 'tags', disableSort: true, disableFilterMenu: true, width: 220, key: "tags", visible: false },
|
||||
{ name: 'Comment', type: 'string', key: 'comment', width: 200, visible: false },
|
||||
];
|
||||
export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
const [updateSetting] = useSetMythicSetting();
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [allData, setAllData] = React.useState([]);
|
||||
const [openContextMenu, setOpenContextMenu] = React.useState(false);
|
||||
const [filterOptions, setFilterOptions] = React.useState({});
|
||||
const [selectedColumn, setSelectedColumn] = React.useState({});
|
||||
const [sortData, setSortData] = React.useState({"sortKey": null, "sortDirection": null, "sortType": null})
|
||||
const [columnVisibility, setColumnVisibility] = React.useState({
|
||||
"visible": ["Info", "Name", "Size", "Last Modify"],
|
||||
"visible": defaultVisibleColumns,
|
||||
"hidden": ["Tags", "Comment"]
|
||||
});
|
||||
const [openAdjustColumnsDialog, setOpenAdjustColumnsDialog] = React.useState(false);
|
||||
const [openReorderDialog, setOpenReorderDialog] = React.useState(false);
|
||||
const [columnOrder, setColumnOrder] = React.useState(columnDefaults);
|
||||
const [selectedRows, setSelectedRows] = React.useState([]);
|
||||
const columns = React.useMemo(
|
||||
() =>
|
||||
[
|
||||
{ name: 'Info', width: 65, disableDoubleClick: true, disableSort: true, disableFilterMenu: true },
|
||||
{ name: 'Name', type: 'string', key: 'name_text', fillWidth: true },
|
||||
{ name: "Size", type: "size", key: "size", inMetadata: true, width: 100},
|
||||
{ name: "Last Modify", type: "date", key: "modify_time", inMetadata: true, width: 250},
|
||||
{ name: 'Tags', type: 'tags', disableSort: true, disableFilterMenu: true, width: 220 },
|
||||
{ name: 'Comment', type: 'string', key: 'comment', width: 200 },
|
||||
].reduce( (prev, cur) => {
|
||||
columnOrder.reduce( (prev, cur) => {
|
||||
if(columnVisibility.visible.includes(cur.name)){
|
||||
if(filterOptions[cur.key] && String(filterOptions[cur.key]).length > 0){
|
||||
return [...prev, {...cur, filtered: true}];
|
||||
@@ -108,7 +116,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
return [...prev];
|
||||
}
|
||||
}, [])
|
||||
, [filterOptions, columnVisibility]
|
||||
, [filterOptions, columnVisibility, columnOrder]
|
||||
);
|
||||
const sortedData = React.useMemo(() => {
|
||||
if (sortData.sortKey === null || sortData.sortType === null) {
|
||||
@@ -188,10 +196,22 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
}
|
||||
return tempData;
|
||||
}, [allData, sortData]);
|
||||
const onSubmitFilterOptions = (newFilterOptions) => {
|
||||
setFilterOptions(newFilterOptions);
|
||||
const onSubmitFilterOptions = (value) => {
|
||||
|
||||
try{
|
||||
let newValue = value ? value : "";
|
||||
setFilterOptions({...filterOptions, [selectedColumn.key]: newValue });
|
||||
updateSetting({setting_name: `file_browser_filter_options`, value: {...filterOptions,[selectedColumn.key]: newValue }});
|
||||
}catch(error){
|
||||
console.log("failed to save filter options", error);
|
||||
}
|
||||
}
|
||||
const filterRow = (row) => {
|
||||
if(props.treeRootData[props.selectedFolderData.group] === undefined ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host] === undefined ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row] === undefined) {
|
||||
return true;
|
||||
}
|
||||
if(!props.showDeletedFiles && props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.deleted){
|
||||
return true;
|
||||
}
|
||||
@@ -216,7 +236,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
treeRootData={props.treeRootData[props.selectedFolderData.group]}
|
||||
selectedFolderData={props.selectedFolderData}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}
|
||||
cellData={row}
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.name_text}
|
||||
me={props.me}
|
||||
tabInfo={props.tabInfo}
|
||||
onTaskRowAction={props.onTaskRowAction} />;
|
||||
@@ -224,7 +244,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
return <FileBrowserTableRowNameCell
|
||||
treeRootData={props.treeRootData[props.selectedFolderData.group]}
|
||||
selectedFolderData={props.selectedFolderData}
|
||||
cellData={row}
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.name_text}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]} />;
|
||||
case "Size":
|
||||
return <TableRowSizeCell cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.metadata?.size}
|
||||
@@ -254,9 +274,9 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
return "showTask";
|
||||
}else if(sortedData.length > 0 || props?.selectedFolderData?.success){
|
||||
return "normal";
|
||||
} else if(sortedData.length === 0 && props?.selectedFolderData?.metadata?.has_children && props?.selectedFolderData?.success === null){
|
||||
} else if(sortedData.length === 0 && props?.selectedFolderData?.has_children && props?.selectedFolderData?.success === null){
|
||||
return "fetchLocal";
|
||||
}else if(sortedData.length === 0 && !props?.selectedFolderData?.metadata?.has_children && props?.selectedFolderData?.success === null) {
|
||||
}else if(sortedData.length === 0 && !props?.selectedFolderData?.has_children && props?.selectedFolderData?.success === null) {
|
||||
return "fetchRemote";
|
||||
}else if(sortedData.length === 0){
|
||||
return "fetchRemote";
|
||||
@@ -304,8 +324,12 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
};
|
||||
const contextMenuOptions = [
|
||||
{
|
||||
name: 'Filter Column',
|
||||
name: 'Filter Column', type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
if(columns[columnIndex].disableFilterMenu){
|
||||
snackActions.warning("Can't filter that column");
|
||||
return;
|
||||
@@ -315,14 +339,84 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Show/Hide Columns",
|
||||
name: "Reorder Columns and Adjust Visibility",type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
setOpenAdjustColumnsDialog(true);
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
setOpenReorderDialog(true);
|
||||
}
|
||||
}
|
||||
];
|
||||
const onSubmitAdjustColumns = ({left, right}) => {
|
||||
setColumnVisibility({visible: right, hidden: left});
|
||||
React.useEffect( () => {
|
||||
try {
|
||||
const storageItem = GetMythicSetting({setting_name: `file_browser_table_columns`, default_value: defaultVisibleColumns});
|
||||
if(storageItem !== null){
|
||||
let allColumns = [...columnVisibility["visible"].map(c => c), ...columnVisibility["hidden"].map(c => c)];
|
||||
let newHidden = [];
|
||||
allColumns.forEach((v,i,a) => {
|
||||
if(!storageItem.includes(v)){
|
||||
newHidden.push(v);
|
||||
}
|
||||
});
|
||||
if(storageItem.length !== 0){
|
||||
setColumnVisibility({visible: storageItem, hidden: newHidden});
|
||||
}
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_columns", error);
|
||||
}
|
||||
try {
|
||||
const storageItemOptions = GetMythicSetting({setting_name: `file_browser_filter_options`, default_value: {}});
|
||||
if(storageItemOptions !== null){
|
||||
setFilterOptions(storageItemOptions);
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_filter_options", error);
|
||||
}
|
||||
try {
|
||||
const storageColumnOrder = GetMythicSetting({setting_name: `file_browser_column_order`, default_value: columns.map(c => c.name)});
|
||||
if(storageColumnOrder !== null){
|
||||
let newOrder = [];
|
||||
for(let i = 0; i < storageColumnOrder.length; i++){
|
||||
for(let j = 0; j < columnOrder.length; j++){
|
||||
if(columnOrder[j].name === storageColumnOrder[i]){
|
||||
newOrder.push(columnOrder[j]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
setColumnOrder(newOrder);
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load file_browser_table_filter_options", error);
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
const onSubmitColumnReorder = (newOrder) => {
|
||||
let newVisible = [];
|
||||
let newHidden = [];
|
||||
for(let i = 0; i < newOrder.length; i++){
|
||||
if(newOrder[i].visible){
|
||||
newVisible.push(newOrder[i].name);
|
||||
} else {
|
||||
newHidden.push(newOrder[i].name);
|
||||
}
|
||||
}
|
||||
if(newVisible.length === 0){
|
||||
snackActions.error("Can't update to show no fields");
|
||||
return;
|
||||
}
|
||||
console.log("newOrder", newOrder)
|
||||
updateSetting({setting_name: `file_browser_column_order`, value: newOrder.map(c => c.name)});
|
||||
setColumnOrder(newOrder);
|
||||
setColumnVisibility({visible: newVisible, hidden: newHidden});
|
||||
updateSetting({setting_name: `file_browser_table_columns`, value: newVisible});
|
||||
setOpenReorderDialog(false);
|
||||
}
|
||||
const onResetColumnReorder = () => {
|
||||
onSubmitColumnReorder(columnDefaults);
|
||||
}
|
||||
const sortColumn = columns.findIndex((column) => column.key === sortData.sortKey);
|
||||
const onRowClick = ({event, rowDataStatic}) => {
|
||||
@@ -357,7 +451,9 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
copyStringToClipboard(element.name_text);
|
||||
if(copyStringToClipboard(element.name_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -365,22 +461,26 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
copyStringToClipboard(element.full_path_text);
|
||||
if(copyStringToClipboard(element.full_path_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Attributes', type: "item",
|
||||
name: 'Metadata', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
copyStringToClipboard(JSON.stringify(element?.metadata?.permissions, null, 2));
|
||||
if(copyStringToClipboard(JSON.stringify(element?.metadata?.permissions, null, 2))){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
async function optionsB (callback_id, callback_display_id, element) {
|
||||
let downloadCommand = await props.getLoadedCommandForUIFeature(callback_id, element.can_have_children ? "file_browser:download_folder" : "file_browser:download");
|
||||
let downloadCommand = await props.getLoadedCommandForUIFeature(callback_id, element.can_have_children ? props.baseUIFeature + ":download_folder" : props.baseUIFeature + ":download");
|
||||
let downloadDisplay = "Download (Unsupported)";
|
||||
if(element.can_have_children){
|
||||
downloadDisplay = "Download Folder (Unsupported)";
|
||||
@@ -391,15 +491,21 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
downloadDisplay = `Download Folder (${downloadCommand.command.cmd})`;
|
||||
}
|
||||
}
|
||||
let listCommand = await props.getLoadedCommandForUIFeature(callback_id, "file_browser:list");
|
||||
let listCommand = await props.getLoadedCommandForUIFeature(callback_id, props.baseUIFeature + ":list");
|
||||
let listDisplay = "List (Unsupported)";
|
||||
if(listCommand !== undefined){
|
||||
listDisplay = `List (${listCommand.command.cmd})`;
|
||||
}
|
||||
let removeCommand = await props.getLoadedCommandForUIFeature(callback_id, "file_browser:remove");
|
||||
let removeCommand = await props.getLoadedCommandForUIFeature(callback_id, element.can_have_children ? props.baseUIFeature + ":remove_folder" : props.baseUIFeature + ":remove");
|
||||
let removeDisplay = "Remove (Unsupported)";
|
||||
if( element.can_have_children) {
|
||||
removeDisplay = "Remove Folder (Unsupported)"
|
||||
}
|
||||
if(removeCommand !== undefined){
|
||||
removeDisplay = `Remove (${removeCommand.command.cmd})`;
|
||||
if(element.can_have_children){
|
||||
removeDisplay = `Remove Folder (${removeCommand.command.cmd})`;
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
@@ -413,7 +519,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: element.can_have_children ? 'file_browser:download_folder' : 'file_browser:download',
|
||||
uifeature: element.can_have_children ? props.baseUIFeature + ':download_folder' : props.baseUIFeature + ':download',
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
},
|
||||
@@ -428,7 +534,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: 'file_browser:list',
|
||||
uifeature: props.baseUIFeature + ':list',
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
},
|
||||
@@ -443,7 +549,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: 'file_browser:remove',
|
||||
uifeature: element.can_have_children ? props.baseUIFeature + ':remove_folder' : props.baseUIFeature + ':remove',
|
||||
getConfirmation: true,
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
@@ -468,7 +574,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
full_path: selectedRows[i].full_path_text,
|
||||
host: selectedRows[i].host,
|
||||
filename: selectedRows[i].name_text,
|
||||
uifeature: selectedRows[i].can_have_children ? 'file_browser:download_folder': 'file_browser:download',
|
||||
uifeature: selectedRows[i].can_have_children ? props.baseUIFeature + ':download_folder': props.baseUIFeature + ':download',
|
||||
callback_id: props.tabInfo.callbackID,
|
||||
callback_display_id: props.tabInfo.displayID
|
||||
});
|
||||
@@ -488,7 +594,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
full_path: selectedRows[i].full_path_text,
|
||||
host: selectedRows[i].host,
|
||||
filename: selectedRows[i].name_text,
|
||||
uifeature: 'file_browser:remove',
|
||||
uifeature: props.baseUIFeature + ':remove',
|
||||
getConfirmation: true,
|
||||
callback_id: props.tabInfo.callbackID,
|
||||
callback_display_id: props.tabInfo.displayID
|
||||
@@ -509,7 +615,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
full_path: selectedRows[i].full_path_text,
|
||||
host: selectedRows[i].host,
|
||||
filename: selectedRows[i].name_text,
|
||||
uifeature: 'file_browser:list',
|
||||
uifeature: props.baseUIFeature + ':list',
|
||||
callback_id: props.tabInfo.callbackID,
|
||||
callback_display_id: props.tabInfo.displayID
|
||||
});
|
||||
@@ -531,6 +637,21 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
}
|
||||
return options;
|
||||
}
|
||||
if(loading){
|
||||
return (
|
||||
<div style={{width: '100%', height: '100%', position: "relative",}}>
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%"
|
||||
}}>
|
||||
{"Loading Saved Browser Customizations..."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div style={{width: '100%', height: '100%', overflow: "hidden", position: "relative"}}>
|
||||
{displayFormat === "normal" &&
|
||||
@@ -611,26 +732,34 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
<RenderSingleTask task_id={props.selectedFolderData?.task_id} />
|
||||
}
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xs" open={openContextMenu}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
innerDialog={<TableFilterDialog
|
||||
selectedColumn={selectedColumn}
|
||||
filterOptions={filterOptions}
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
onClose={()=>{setOpenContextMenu(false);}} />}
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={filterOptions[selectedColumn.key]}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
{openAdjustColumnsDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openAdjustColumnsDialog}
|
||||
onClose={()=>{setOpenAdjustColumnsDialog(false);}}
|
||||
innerDialog={
|
||||
<MythicTransferListDialog onClose={()=>{setOpenAdjustColumnsDialog(false);}}
|
||||
onSubmit={onSubmitAdjustColumns} right={columnVisibility.visible} rightTitle="Show these columns"
|
||||
leftTitle={"Hidden Columns"} left={columnVisibility.hidden} dialogTitle={"Edit which columns are shown"}/>}
|
||||
{openReorderDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openReorderDialog}
|
||||
onClose={()=>{setOpenReorderDialog(false);}}
|
||||
innerDialog={
|
||||
<CallbacksTableColumnsReorderDialog
|
||||
onClose={()=>{setOpenReorderDialog(false);}}
|
||||
visible={columnVisibility.visible}
|
||||
hidden={columnVisibility.hidden}
|
||||
onReset={onResetColumnReorder}
|
||||
onSubmit={onSubmitColumnReorder}
|
||||
initialItems={columnOrder}
|
||||
/>}
|
||||
/>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -638,8 +767,8 @@ const FileBrowserTableRowNameCell = ({cellData, rowData, treeRootData, selected
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div style={{ alignItems: 'center', display: 'flex', maxHeight: "100%", textDecoration: treeRootData[selectedFolderData.host][cellData]?.deleted ? 'line-through' : '' }}>
|
||||
{!treeRootData[selectedFolderData.host][cellData]?.can_have_children ? (
|
||||
<div style={{ alignItems: 'center', display: 'flex', maxHeight: "100%", textDecoration: treeRootData[selectedFolderData.host][rowData.full_path_text]?.deleted ? 'line-through' : '' }}>
|
||||
{!treeRootData[selectedFolderData.host][rowData.full_path_text]?.can_have_children ? (
|
||||
<DescriptionIcon size={"small"} style={{
|
||||
marginRight: '5px' }} />
|
||||
) : (
|
||||
@@ -650,7 +779,7 @@ const FileBrowserTableRowNameCell = ({cellData, rowData, treeRootData, selected
|
||||
marginRight: '8px',
|
||||
marginLeft: "4px",
|
||||
color:
|
||||
treeRootData[selectedFolderData.host][cellData]?.success || treeRootData[selectedFolderData.host][cellData]?.metadata?.has_children
|
||||
treeRootData[selectedFolderData.host][rowData.full_path_text]?.success || treeRootData[selectedFolderData.host][rowData.full_path_text]?.has_children
|
||||
? theme.folderColor
|
||||
: 'grey',
|
||||
}}
|
||||
@@ -659,17 +788,17 @@ const FileBrowserTableRowNameCell = ({cellData, rowData, treeRootData, selected
|
||||
<pre
|
||||
style={{
|
||||
color:
|
||||
treeRootData[selectedFolderData.host][cellData]?.success !== null
|
||||
treeRootData[selectedFolderData.host][rowData.full_path_text]?.success !== null
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary,
|
||||
}}>
|
||||
{treeRootData[selectedFolderData.host][cellData]?.name_text}
|
||||
{cellData}
|
||||
</pre>
|
||||
{treeRootData[selectedFolderData.host][cellData]?.success === true ? (
|
||||
{treeRootData[selectedFolderData.host][rowData.full_path_text]?.success === true ? (
|
||||
<MythicStyledTooltip title='Successfully listed contents of folder' tooltipStyle={{display: "inline-flex", marginLeft: "5px"}}>
|
||||
<CheckCircleOutlineIcon color="success" fontSize='small' />
|
||||
</MythicStyledTooltip>
|
||||
) : treeRootData[selectedFolderData.host][cellData]?.success === false ? (
|
||||
) : treeRootData[selectedFolderData.host][rowData.full_path_text]?.success === false ? (
|
||||
<MythicStyledTooltip title='Failed to list contents of folder' tooltipStyle={{display: "inline-flex", marginLeft: "5px"}}>
|
||||
<ErrorIcon fontSize='small' color="error" />
|
||||
</MythicStyledTooltip>
|
||||
@@ -802,7 +931,7 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
|
||||
};
|
||||
const optionsA = [
|
||||
{
|
||||
name: treeRootData[selectedFolderData.host][cellData]?.name_text, icon: null, click: ({event}) => {},
|
||||
name: treeRootData[selectedFolderData.host][rowData.full_path_text]?.name_text, icon: null, click: ({event}) => {},
|
||||
type: "item", disabled: true
|
||||
},
|
||||
{
|
||||
@@ -810,7 +939,7 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
|
||||
icon: <VisibilityIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
setPermissionData(treeRootData[selectedFolderData.host][cellData].metadata);
|
||||
setPermissionData(treeRootData[selectedFolderData.host][rowData.full_path_text].metadata);
|
||||
setViewPermissionsDialogOpen(true);
|
||||
},
|
||||
},
|
||||
@@ -820,7 +949,7 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
getHistory({ variables: {
|
||||
full_path_text: treeRootData[selectedFolderData.host][cellData].full_path_text,
|
||||
full_path_text: treeRootData[selectedFolderData.host][rowData.full_path_text].full_path_text,
|
||||
host: selectedFolderData.host,
|
||||
group: [selectedFolderData.group],
|
||||
} });
|
||||
@@ -859,8 +988,8 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
|
||||
ref={dropdownAnchorRef}>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
{treeRootData[selectedFolderData.host][cellData]?.filemeta.length > 0 ?
|
||||
<MythicStyledTooltip title={treeRootData[selectedFolderData.host][cellData]?.filemeta[0]?.complete ?
|
||||
{treeRootData[selectedFolderData.host][rowData.full_path_text]?.filemeta.length > 0 ?
|
||||
<MythicStyledTooltip title={treeRootData[selectedFolderData.host][rowData.full_path_text]?.filemeta[0]?.complete ?
|
||||
"Preview Media" : "Preview Partial Media"}>
|
||||
<FontAwesomeIcon icon={faPhotoVideo} size={"1x"} style={{
|
||||
position: "relative", cursor: "pointer", display: "inline-block"}}
|
||||
@@ -872,8 +1001,8 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openPreviewMediaDialog}
|
||||
onClose={(e)=>{setOpenPreviewMediaDialog(false);}}
|
||||
innerDialog={<PreviewFileMediaDialog
|
||||
agent_file_id={treeRootData[selectedFolderData.host][cellData]?.filemeta[0]?.agent_file_id}
|
||||
filename={b64DecodeUnicode(treeRootData[selectedFolderData.host][cellData]?.filemeta[0]?.filename_text)}
|
||||
agent_file_id={treeRootData[selectedFolderData.host][rowData.full_path_text]?.filemeta[0]?.agent_file_id}
|
||||
filename={b64DecodeUnicode(treeRootData[selectedFolderData.host][rowData.full_path_text]?.filemeta[0]?.filename_text)}
|
||||
onClose={(e)=>{setOpenPreviewMediaDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -6,11 +6,14 @@ import ListIcon from '@mui/icons-material/List';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import {getAllParentNodes} from "./CallbacksTabsFileBrowser";
|
||||
import {Dropdown, DropdownMenuItem, DropdownNestedMenuItem} from "../../MythicComponents/MythicNestedMenus";
|
||||
import {copyStringToClipboard} from "../../utilities/Clipboard";
|
||||
import FileCopyOutlinedIcon from '@mui/icons-material/FileCopyOutlined';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
|
||||
export const getOpenIDFromNode = (node) => {
|
||||
return `${node.group};${node.host};${node.full_path_text}`;
|
||||
}
|
||||
export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatrix, fetchFolderData, setTableData, taskListing, tableOpenedPathId, showDeletedFiles, tabInfo, selectedFolderData}) => {
|
||||
export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatrix, fetchFolderData, setTableData, taskListing, tableOpenedPathId, showDeletedFiles, tabInfo, selectedFolderData, baseUIFeature}) => {
|
||||
const [openNodes, setOpenNodes] = React.useState({});
|
||||
const groupName = React.useRef("");
|
||||
const [openViewGroupsDialog, setOpenViewGroupDialog] = React.useState(false);
|
||||
@@ -19,7 +22,7 @@ export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatri
|
||||
//console.log("toggleNodeExpanded", nodeId, nodeData);
|
||||
let lastOpenedNodeNewState = true;
|
||||
let lastOpenedNodeID = getOpenIDFromNode(lastOpenedNodeRef.current);
|
||||
if(!lastOpenedNodeRef.current?.metadata?.has_children){
|
||||
if(!lastOpenedNodeRef.current?.has_children){
|
||||
lastOpenedNodeNewState = false;
|
||||
}
|
||||
lastOpenedNodeRef.current = {...nodeData};
|
||||
@@ -28,7 +31,7 @@ export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatri
|
||||
setOpenNodes({
|
||||
...openNodes,
|
||||
[getOpenIDFromNode(nodeData)]: true,
|
||||
[lastOpenedNodeID]: lastOpenedNodeNewState
|
||||
//[lastOpenedNodeID]: lastOpenedNodeNewState
|
||||
});
|
||||
};
|
||||
const toggleNodeCollapsed = (nodeId, nodeData) => {
|
||||
@@ -77,6 +80,41 @@ export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatri
|
||||
});
|
||||
}, [selectedFolderData]);
|
||||
const contextMenuOptions= (callback_id, callback_display_id, node) => [
|
||||
{
|
||||
name: "Copy to Clipboard", icon: null, click: () => {}, type: "menu",
|
||||
menuItems: [
|
||||
{
|
||||
name: 'Name', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(node.name_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Full Path', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(node.full_path_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Metadata', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(JSON.stringify(node?.metadata, null, 2))){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'List', type: "item", icon: <ListIcon color="warning" style={{ paddingRight: '5px'}} />,
|
||||
click: ({event}) => {
|
||||
@@ -146,6 +184,7 @@ export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatri
|
||||
onCollapseNode={toggleNodeCollapsed}
|
||||
onContextMenu={onContextMenu}
|
||||
tabInfo={tabInfo}
|
||||
baseUIFeature={baseUIFeature}
|
||||
/>
|
||||
{openViewGroupsDialog &&
|
||||
<MythicDialog
|
||||
|
||||
@@ -8,6 +8,7 @@ import {hideCallbacksMutation} from './CallbackMutations';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import {CallbacksTableLastCheckinCell, CallbacksTablePayloadTypeCell, CallbacksTableIPCell} from "./CallbacksTableRow";
|
||||
import { DataGrid } from '@mui/x-data-grid';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
|
||||
|
||||
const callbacksAndFeaturesQuery = gql`
|
||||
@@ -92,7 +93,7 @@ const columns = [
|
||||
flex: 0.5,
|
||||
}
|
||||
];
|
||||
const CustomSelectTable = ({initialData, selectedData}) => {
|
||||
const CustomSelectTable = ({initialData, selectedData, sortModel}) => {
|
||||
const [data, setData] = React.useState([]);
|
||||
const [rowSelectionModel, setRowSelectionModel] = React.useState({
|
||||
type: 'include',
|
||||
@@ -120,7 +121,7 @@ const CustomSelectTable = ({initialData, selectedData}) => {
|
||||
},
|
||||
},
|
||||
sorting: {
|
||||
sortModel: [{ field: 'last_checkin', sort: 'asc' }],
|
||||
sortModel: [sortModel],
|
||||
},
|
||||
}}
|
||||
autoPageSize
|
||||
@@ -175,8 +176,13 @@ export function CallbacksTabsHideMultipleDialog({onClose}) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Hide Multiple Callbacks at Once</DialogTitle>
|
||||
<DialogContentText style={{textAlign: "center"}}>
|
||||
<b>Note: </b> Last checkin times are based on when this window opened and won't refresh.
|
||||
</DialogContentText>
|
||||
<CustomSelectTable initialData={initialData}
|
||||
selectedData={selectedData} />
|
||||
selectedData={selectedData}
|
||||
sortModel={{ field: 'last_checkin', sort: 'asc' }}
|
||||
/>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant="contained" color="primary">
|
||||
Close
|
||||
@@ -188,4 +194,49 @@ export function CallbacksTabsHideMultipleDialog({onClose}) {
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
export function CallbacksTabsSelectMultipleDialog({onClose, onSubmit}) {
|
||||
|
||||
const selectedData = React.useRef([]);
|
||||
const [initialData, setInitialData] = React.useState([]);
|
||||
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>
|
||||
<CustomSelectTable initialData={initialData}
|
||||
selectedData={selectedData}
|
||||
sortModel={{ field: 'display_id', sort: 'desc' }}
|
||||
/>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant="contained" color="primary">
|
||||
Close
|
||||
</Button>
|
||||
<Button onClick={submitTasking} variant="contained" color="warning">
|
||||
Use Selection
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import DialogTitle from '@mui/material/DialogTitle';
|
||||
import {useQuery, gql } from '@apollo/client';
|
||||
import {CallbacksTableLastCheckinCell, CallbacksTablePayloadTypeCell, CallbacksTableIPCell} from "./CallbacksTableRow";
|
||||
import { DataGrid } from '@mui/x-data-grid';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
|
||||
|
||||
const callbacksAndFeaturesQuery = gql`
|
||||
@@ -140,6 +141,9 @@ export function CallbacksTabsOpenMultipleDialog({onClose, tabType, onOpenTabs})
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Select Callback to open {tabType} tab</DialogTitle>
|
||||
<DialogContentText style={{textAlign: "center"}}>
|
||||
<b>Note: </b> Last checkin times are based on when this window opened and won't refresh.
|
||||
</DialogContentText>
|
||||
<CustomSelectTable initialData={initialData}
|
||||
selectedData={selectedData}
|
||||
onRowClick={onRowClick}
|
||||
|
||||
@@ -6,8 +6,6 @@ import {useTheme} from '@mui/material/styles';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import {CallbacksTabsProcessBrowserTable} from './CallbacksTabsProcessBrowserTable';
|
||||
import {MythicModifyStringDialog} from '../../MythicComponents/MythicDialog';
|
||||
import {TaskFromUIButton} from './TaskFromUIButton';
|
||||
@@ -132,7 +130,7 @@ export function CallbacksTabsProcessBrowserLabel(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} open={openEditDescriptionDialog} onClose={() => {setOpenEditDescriptionDialog(false);}}
|
||||
innerDialog={
|
||||
@@ -143,7 +141,7 @@ export function CallbacksTabsProcessBrowserLabel(props){
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>{
|
||||
export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me, setNewDataForTab}) =>{
|
||||
const fromNow = React.useRef((new Date()));
|
||||
const [backdropOpen, setBackdropOpen] = React.useState(false);
|
||||
const [expandOrCollapseAll, setExpandOrCollapseAll] = React.useState(false);
|
||||
@@ -271,11 +269,14 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>
|
||||
treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]].callbacks = [data.data.mythictree_stream[i].callback]
|
||||
} else {
|
||||
// we need to merge data in because we already have some info
|
||||
if(data.data.mythictree_stream[i].deleted){
|
||||
delete treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]];
|
||||
continue;
|
||||
}
|
||||
let existingData = treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]];
|
||||
if(existingData.task_id <= data.data.mythictree_stream[i].task_id){
|
||||
if(data.data.mythictree_stream[i].deleted){
|
||||
delete treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
existingData.has_children = data.data.mythictree_stream[i].has_children || existingData.has_children;
|
||||
existingData.callbacks.push(data.data.mythictree_stream[i].callback)
|
||||
existingData.comment += data.data.mythictree_stream[i].comment;
|
||||
existingData.tags = [...existingData.tags, ...data.data.mythictree_stream[i].tags];
|
||||
@@ -290,6 +291,9 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>
|
||||
}
|
||||
const newMatrix = getNewMatrix();
|
||||
setTreeAdjMtx(newMatrix);
|
||||
if(index !== value){
|
||||
setNewDataForTab((prev) => {return {...prev, [tabInfo.tabID]: true}});
|
||||
}
|
||||
}
|
||||
});
|
||||
const getHostProcessesQuerySuccess = (data) => {
|
||||
|
||||
@@ -9,8 +9,6 @@ import ListIcon from '@mui/icons-material/List';
|
||||
import { snackActions } from '../../utilities/Snackbar';
|
||||
import 'react-virtualized/styles.css';
|
||||
import MythicResizableGrid from '../../MythicComponents/MythicResizableGrid';
|
||||
import {TableFilterDialog} from './TableFilterDialog';
|
||||
import {MythicTransferListDialog} from '../../MythicComponents/MythicTransferList';
|
||||
import {TagsDisplay, ViewEditTags} from '../../MythicComponents/MythicTag';
|
||||
import TerminalIcon from '@mui/icons-material/Terminal';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
@@ -22,7 +20,12 @@ import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
import {Dropdown, DropdownMenuItem, DropdownNestedMenuItem} from "../../MythicComponents/MythicNestedMenus";
|
||||
import {faSkullCrossbones, faSyringe, faKey,} from '@fortawesome/free-solid-svg-icons';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {GetComputedFontSize} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {
|
||||
GetComputedFontSize,
|
||||
GetMythicSetting,
|
||||
useSetMythicSetting
|
||||
} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {CallbacksTableColumnsReorderDialog} from "./CallbacksTableColumnsReorderDialog";
|
||||
|
||||
const getPermissionsDataQuery = gql`
|
||||
query getPermissionsQuery($mythictree_id: Int!) {
|
||||
@@ -40,24 +43,40 @@ const updateFileComment = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const columnDefaults = [
|
||||
{ name: 'Info', width: 50, disableAutosize: true, disableSort: true, disableFilterMenu: true, key: "info", visible: true },
|
||||
{ name: 'PID', type: 'number', key: 'process_id', inMetadata: true, width: 100, visible: true},
|
||||
{ name: 'PPID', type: 'number', key: 'parent_process_id', inMetadata: true, width: 100, visible: true},
|
||||
{ name: 'Name', type: 'string', disableSort: false, key: 'name_text', fillWidth: true, visible: true },
|
||||
{ name: "Arch", type: 'string', key: 'architecture', inMetadata: true, width: 70, visible: true},
|
||||
{ name: "Integrity", type: "number", key: "integrity_level", inMetadata: true, width: 100, visible: false },
|
||||
{ name: 'Session', type: 'number', key: 'session_id', inMetadata: true, width: 100, visible: true},
|
||||
{ name: "User", type: 'string', key: 'user', inMetadata: true, fillWidth: true, visible: true},
|
||||
{ name: 'Tags', type: 'tags', disableSort: true, disableFilterMenu: true, width: 220, key: "tags", visible: false },
|
||||
{ name: 'Comment', type: 'string', key: 'comment', disableSort: false, width: 200, visible: false },
|
||||
{ name: "CMD", type: "string", key: 'command_line', inMetadata: true, fillWidth: true, visible: true},
|
||||
];
|
||||
const defaultVisibleColumns = ["Info","PID", "PPID", "Name", "Arch", "Session", "User", "CMD"];
|
||||
export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, me, onRowDoubleClick,
|
||||
onTaskRowAction, host, group, showDeletedFiles, tabInfo,
|
||||
expandOrCollapseAll, getLoadedCommandForUIFeature}) => {
|
||||
//const [allData, setAllData] = React.useState([]);
|
||||
//console.log("treeAdjMatrix updated in table", treeAdjMatrix)
|
||||
const [updateSetting] = useSetMythicSetting();
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [sortData, setSortData] = React.useState({"sortKey": null, "sortDirection": null, "sortType": null});
|
||||
const [openNodes, setOpenNodes] = React.useState({});
|
||||
const [openContextMenu, setOpenContextMenu] = React.useState(false);
|
||||
const [filterOptions, setFilterOptions] = React.useState({});
|
||||
const selectedColumn = React.useRef({});
|
||||
const [columnVisibility, setColumnVisibility] = React.useState({
|
||||
"visible": ["Info","PID", "PPID", "Name", "User", "Arch", "CMD"],
|
||||
"hidden": [ "Session", "Comment", "Tags" ]
|
||||
"visible": defaultVisibleColumns,
|
||||
"hidden": [ "Comment", "Tags" ]
|
||||
})
|
||||
const [singleTreeData, setSingleTreeData] = React.useState({});
|
||||
const [viewSingleTreeData, setViewSingleTreeData] = React.useState(false);
|
||||
const [openAdjustColumnsDialog, setOpenAdjustColumnsDialog] = React.useState(false);
|
||||
const [openReorderDialog, setOpenReorderDialog] = React.useState(false);
|
||||
const [columnOrder, setColumnOrder] = React.useState(columnDefaults);
|
||||
const [updatedTreeAdjMatrix, setUpdatedTreeAdjMatrix] = React.useState(treeAdjMatrix);
|
||||
const openAllNodes = (state) => {
|
||||
let onodes = {};
|
||||
@@ -192,21 +211,10 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
// }
|
||||
|
||||
};
|
||||
const columnDefaults = [
|
||||
{ name: 'Info', width: 50, disableAutosize: true, disableSort: true, disableFilterMenu: true },
|
||||
{ name: 'PID', type: 'number', key: 'process_id', inMetadata: true, width: 100},
|
||||
{ name: 'PPID', type: 'number', key: 'parent_process_id', inMetadata: true, width: 100},
|
||||
{ name: 'Name', type: 'string', disableSort: false, key: 'name_text', fillWidth: true },
|
||||
{ name: "User", type: 'string', key: 'user', inMetadata: true, fillWidth: true},
|
||||
{ name: "Arch", type: 'string', key: 'architecture', inMetadata: true, width: 100},
|
||||
{ name: 'Tags', type: 'tags', disableSort: true, disableFilterMenu: true, width: 220 },
|
||||
{ name: 'Comment', type: 'string', key: 'comment', disableSort: false, width: 200 },
|
||||
{ name: "CMD", type: "string", key: 'command_line', inMetadata: true, fillWidth: true},
|
||||
{ name: 'Session', type: 'number', key: 'session_id', inMetadata: true, width: 100}
|
||||
];
|
||||
|
||||
const columns = React.useMemo(
|
||||
() =>
|
||||
columnDefaults.reduce( (prev, cur) => {
|
||||
() =>
|
||||
columnOrder.reduce( (prev, cur) => {
|
||||
if(columnVisibility.visible.includes(cur.name)){
|
||||
if(filterOptions[cur.key] && String(filterOptions[cur.key]).length > 0){
|
||||
return [...prev, {...cur, filtered: true}];
|
||||
@@ -217,7 +225,7 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
return [...prev];
|
||||
}
|
||||
}, [])
|
||||
, [filterOptions, columnVisibility]
|
||||
, [filterOptions, columnVisibility, columnOrder]
|
||||
);
|
||||
const flattenNode = useCallback(
|
||||
(node, host, group, depth = 0) => {
|
||||
@@ -384,8 +392,14 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
}
|
||||
return tempData;
|
||||
}, [allData, sortData]);
|
||||
const onSubmitFilterOptions = (newFilterOptions) => {
|
||||
setFilterOptions(newFilterOptions);
|
||||
const onSubmitFilterOptions = (value) => {
|
||||
let newValue = value ? value : "";
|
||||
try{
|
||||
setFilterOptions({...filterOptions, [selectedColumn.current.key]: newValue });
|
||||
updateSetting({setting_name: `process_browser_filter_options`, value: {...filterOptions,[selectedColumn.current.key]: newValue }});
|
||||
}catch(error){
|
||||
console.log("failed to save filter options", error, filterOptions, selectedColumn.current.key, newValue);
|
||||
}
|
||||
if(viewSingleTreeData){
|
||||
return
|
||||
}
|
||||
@@ -482,7 +496,7 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
}
|
||||
|
||||
setSingleTreeData(singleTreeAdjMatrix);
|
||||
onSubmitFilterOptions({});
|
||||
onSubmitFilterOptions(undefined);
|
||||
}
|
||||
const toggleViewSingleTreeData = () => {
|
||||
setViewSingleTreeData(!viewSingleTreeData);
|
||||
@@ -550,6 +564,13 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
group={group}
|
||||
rowData={row}
|
||||
cellData={treeRootData[group][host][row.full_path_text]?.parent_path_text || ""} />;
|
||||
case "Integrity":
|
||||
return <FileBrowserTableRowStringCell
|
||||
treeRootData={treeRootData[group]}
|
||||
host={host}
|
||||
group={group}
|
||||
rowData={row}
|
||||
cellData={treeRootData[group][host][row.full_path_text ]?.metadata?.integrity_level || ''} />;
|
||||
case "Tags":
|
||||
return <FileBrowserTagsCell
|
||||
rowData={row}
|
||||
@@ -574,7 +595,7 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
cellData={treeRootData[group][host][row.full_path_text ]?.metadata?.command_line || ""}
|
||||
/>;
|
||||
default:
|
||||
console.log("hit default case in switch on c.name)")
|
||||
console.log("hit default case in switch on c.name)", c.name)
|
||||
}
|
||||
})];
|
||||
}
|
||||
@@ -605,8 +626,12 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
};
|
||||
const contextMenuOptions = [
|
||||
{
|
||||
name: 'Filter Column',
|
||||
name: 'Filter Column', type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
if(columns[columnIndex].disableFilterMenu){
|
||||
snackActions.warning("Can't filter that column");
|
||||
return;
|
||||
@@ -616,18 +641,87 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Show/Hide Columns",
|
||||
name: "Reorder Columns and Adjust Visibility", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
if(columns[columnIndex].disableFilterMenu){
|
||||
snackActions.warning("Can't filter that column");
|
||||
return;
|
||||
}
|
||||
setOpenAdjustColumnsDialog(true);
|
||||
setOpenReorderDialog(true);
|
||||
}
|
||||
}
|
||||
];
|
||||
const onSubmitAdjustColumns = ({left, right}) => {
|
||||
setColumnVisibility({visible: right, hidden: left});
|
||||
React.useEffect( () => {
|
||||
try {
|
||||
const storageItem = GetMythicSetting({setting_name: `process_browser_table_columns`, default_value: defaultVisibleColumns});
|
||||
if(storageItem !== null){
|
||||
let allColumns = [...columnVisibility["visible"].map(c => c), ...columnVisibility["hidden"].map(c => c)];
|
||||
let newHidden = [];
|
||||
allColumns.forEach((v,i,a) => {
|
||||
if(!storageItem.includes(v)){
|
||||
newHidden.push(v);
|
||||
}
|
||||
});
|
||||
if(storageItem.length !== 0){
|
||||
setColumnVisibility({visible: storageItem, hidden: newHidden});
|
||||
}
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_columns", error);
|
||||
}
|
||||
try {
|
||||
const storageItemOptions = GetMythicSetting({setting_name: `process_browser_filter_options`, default_value: {}});
|
||||
if(storageItemOptions !== null){
|
||||
setFilterOptions(storageItemOptions);
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_filter_options", error);
|
||||
}
|
||||
try {
|
||||
const storageColumnOrder = GetMythicSetting({setting_name: `process_browser_column_order`, default_value: columns.map(c => c.name)});
|
||||
if(storageColumnOrder !== null){
|
||||
let newOrder = [];
|
||||
for(let i = 0; i < storageColumnOrder.length; i++){
|
||||
for(let j = 0; j < columnOrder.length; j++){
|
||||
if(columnOrder[j].name === storageColumnOrder[i]){
|
||||
newOrder.push(columnOrder[j]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
setColumnOrder(newOrder);
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load process_browser_table_filter_options", error);
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
const onSubmitColumnReorder = (newOrder) => {
|
||||
let newVisible = [];
|
||||
let newHidden = [];
|
||||
for(let i = 0; i < newOrder.length; i++){
|
||||
if(newOrder[i].visible){
|
||||
newVisible.push(newOrder[i].name);
|
||||
} else {
|
||||
newHidden.push(newOrder[i].name);
|
||||
}
|
||||
}
|
||||
if(newVisible.length === 0){
|
||||
snackActions.error("Can't update to show no fields");
|
||||
return;
|
||||
}
|
||||
updateSetting({setting_name: `process_browser_column_order`, value: newOrder.map(c => c.name)});
|
||||
setColumnOrder(newOrder);
|
||||
setColumnVisibility({visible: newVisible, hidden: newHidden});
|
||||
updateSetting({setting_name: `process_browser_table_columns`, value: newVisible});
|
||||
setOpenReorderDialog(false);
|
||||
}
|
||||
const onResetColumnReorder = () => {
|
||||
onSubmitColumnReorder(columnDefaults);
|
||||
}
|
||||
const sortColumn = columns.findIndex((column) => column.key === sortData.sortKey);
|
||||
React.useEffect( () => {
|
||||
@@ -640,6 +734,21 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
openAllNodes(false);
|
||||
}
|
||||
}, [expandOrCollapseAll, updatedTreeAdjMatrix]);
|
||||
if(loading){
|
||||
return (
|
||||
<div style={{width: '100%', height: '100%', position: "relative",}}>
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%"
|
||||
}}>
|
||||
{"Loading Saved Browser Customizations..."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%', overflow: "hidden", position: "relative" }}>
|
||||
<MythicResizableGrid
|
||||
@@ -653,24 +762,34 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
/>
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xs" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={<TableFilterDialog
|
||||
selectedColumn={selectedColumn.current}
|
||||
filterOptions={filterOptions}
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
onClose={()=>{setOpenContextMenu(false);}} />}
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={String(filterOptions[selectedColumn.current.key])}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
{openAdjustColumnsDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openAdjustColumnsDialog}
|
||||
onClose={()=>{setOpenAdjustColumnsDialog(false);}}
|
||||
innerDialog={
|
||||
<MythicTransferListDialog onClose={()=>{setOpenAdjustColumnsDialog(false);}}
|
||||
onSubmit={onSubmitAdjustColumns} right={columnVisibility.visible} rightTitle="Show these columns"
|
||||
leftTitle={"Hidden Columns"} left={columnVisibility.hidden} dialogTitle={"Edit which columns are shown"}/>}
|
||||
{openReorderDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openReorderDialog}
|
||||
onClose={()=>{setOpenReorderDialog(false);}}
|
||||
innerDialog={
|
||||
<CallbacksTableColumnsReorderDialog
|
||||
onClose={()=>{setOpenReorderDialog(false);}}
|
||||
visible={columnVisibility.visible}
|
||||
hidden={columnVisibility.hidden}
|
||||
onReset={onResetColumnReorder}
|
||||
onSubmit={onSubmitColumnReorder}
|
||||
initialItems={columnOrder}
|
||||
/>}
|
||||
/>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {CallbacksTableIPCell, CallbacksTableLastCheckinCell} from "./CallbacksTa
|
||||
import { DataGrid } from '@mui/x-data-grid';
|
||||
import { validate as uuidValidate } from 'uuid';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
|
||||
|
||||
const callbacksAndFeaturesQuery = gql`
|
||||
@@ -291,12 +292,16 @@ export function CallbacksTabsTaskMultipleDialog({onClose, callback}) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Task Multiple {callback.payload.payloadtype.name} Callbacks at Once</DialogTitle>
|
||||
<DialogContentText style={{textAlign: "center"}}>
|
||||
<b>Note: </b> Last checkin times are based on when this window opened and won't refresh.
|
||||
</DialogContentText>
|
||||
<CustomSelectTable initialData={initialData}
|
||||
selectedData={selectedData} />
|
||||
<Grid size={12}>
|
||||
<CallbacksTabsTaskingInput filterTasks={false} onSubmitFilter={()=>{}} onSubmitCommandLine={onSubmitCommandLine}
|
||||
changeSelectedToken={changeSelectedToken}
|
||||
payloadtype_name={callback.payload.payloadtype.name}
|
||||
hide_context={true}
|
||||
filterOptions={{}} callback_id={callback.id} callback_os={callback.payload.os} parentMountedRef={mountedRef} />
|
||||
</Grid>
|
||||
{openTaskingButton &&
|
||||
|
||||
@@ -48,7 +48,7 @@ export function CallbacksTabsTaskingLabel(props){
|
||||
]);
|
||||
return (
|
||||
<>
|
||||
<MythicTabLabel label={description} onDragTab={props.onDragTab} me={props.me} {...props} contextMenuOptions={contextMenuOptions}/>
|
||||
<MythicTabLabel highlight={props.newDataForTab[props.tabInfo.tabID]} label={description} onDragTab={props.onDragTab} me={props.me} {...props} contextMenuOptions={contextMenuOptions}/>
|
||||
{openEditDescriptionDialog &&
|
||||
<MythicDialog fullWidth={true} open={openEditDescriptionDialog} onClose={() => {setOpenEditDescriptionDialog(false);}}
|
||||
innerDialog={
|
||||
@@ -82,7 +82,7 @@ query getBatchTasking($callback_id: Int!, $offset: Int!, $fetchLimit: Int!){
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, parentMountedRef, me, collapseTaskRequest}) =>{
|
||||
export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, parentMountedRef, me, collapseTaskRequest, setNewDataForTab}) =>{
|
||||
const [taskLimit, setTaskLimit] = React.useState(20);
|
||||
const [scrollToBottom, setScrollToBottom] = React.useState(false);
|
||||
const [openParametersDialog, setOpenParametersDialog] = React.useState(false);
|
||||
@@ -175,6 +175,9 @@ export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, pa
|
||||
if(!fetched){
|
||||
setFetched(true);
|
||||
}
|
||||
if(index !== value && fetched){
|
||||
setNewDataForTab((prev) => {return {...prev, [tabInfo.tabID]: true}});
|
||||
}
|
||||
//console.log("new subscription data in CallbacksTabsTasking", subscriptionData);
|
||||
const oldLength = taskingDataRef.current.task.length;
|
||||
const mergedData = data.data.task.reduce( (prev, cur) => {
|
||||
@@ -270,7 +273,7 @@ export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, pa
|
||||
getInfiniteScrollTasking({variables: {callback_id: tabInfo.callbackID, offset: taskingData.task.length, fetchLimit}});
|
||||
}
|
||||
const onSubmitCommandLine = (message, cmd, parsed, force_parsed_popup, cmdGroupNames, previousTaskingLocation) => {
|
||||
//console.log(message, cmd, parsed);
|
||||
console.log(message, cmd, parsed);
|
||||
let params = message.split(" ");
|
||||
delete params[0];
|
||||
params = params.join(" ").trim();
|
||||
|
||||
@@ -48,7 +48,7 @@ export function CallbacksTabsTaskingConsoleLabel(props){
|
||||
]);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicTabLabel label={description} onDragTab={props.onDragTab} me={props.me} {...props} contextMenuOptions={contextMenuOptions}/>
|
||||
<MythicTabLabel label={description} highlight={props.newDataForTab[props.tabInfo.tabID]} onDragTab={props.onDragTab} me={props.me} {...props} contextMenuOptions={contextMenuOptions}/>
|
||||
{openEditDescriptionDialog &&
|
||||
<MythicDialog fullWidth={true} open={openEditDescriptionDialog} onClose={() => {setOpenEditDescriptionDialog(false);}}
|
||||
innerDialog={
|
||||
@@ -82,7 +82,7 @@ query getBatchTasking($callback_id: Int!, $offset: Int!, $fetchLimit: Int!){
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onCloseTab, parentMountedRef, me}) =>{
|
||||
export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onCloseTab, parentMountedRef, me, setNewDataForTab}) =>{
|
||||
const [taskLimit, setTaskLimit] = React.useState(10);
|
||||
const [scrollToBottom, setScrollToBottom] = React.useState(false);
|
||||
const [openParametersDialog, setOpenParametersDialog] = React.useState(false);
|
||||
@@ -175,6 +175,9 @@ export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onClose
|
||||
if(!fetched){
|
||||
setFetched(true);
|
||||
}
|
||||
if(index !== value && fetched){
|
||||
setNewDataForTab((prev) => {return {...prev, [tabInfo.tabID]: true}});
|
||||
}
|
||||
//console.log("new subscription data in CallbacksTabsTasking", subscriptionData);
|
||||
const oldLength = taskingDataRef.current.task.length;
|
||||
const mergedData = data.data.task.reduce( (prev, cur) => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {CallbacksTabsTaskingFilterDialog} from './CallbacksTabsTaskingFilterDial
|
||||
import {CallbacksTabsTaskingInputTokenSelect} from './CallbacksTabsTaskingInputTokenSelect';
|
||||
import { gql, useSubscription, useMutation } from '@apollo/client';
|
||||
import { snackActions } from '../../utilities/Snackbar';
|
||||
import { meState } from '../../../cache';
|
||||
import {meState, operatorSettingDefaults} from '../../../cache';
|
||||
import {useReactiveVar} from '@apollo/client';
|
||||
import { validate as uuidValidate } from 'uuid';
|
||||
import {MythicSelectFromListDialog} from "../../MythicComponents/MythicSelectFromListDialog";
|
||||
@@ -17,6 +17,9 @@ import {CircularProgress} from '@mui/material';
|
||||
import {getDynamicQueryParams} from "./TaskParametersDialogRow";
|
||||
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
|
||||
import {GetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {getSkewedNow} from "../../utilities/Time";
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
|
||||
const GetLoadedCommandsSubscription = gql`
|
||||
subscription GetLoadedCommandsSubscription($callback_id: Int!){
|
||||
@@ -88,7 +91,23 @@ subscription tasksSubscription($callback_id: Int!){
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const contextSubscription = gql`
|
||||
subscription CallbackMetadataForTasking($callback_id: Int!){
|
||||
callback_stream(batch_size: 1, cursor: {initial_value: {timestamp: "1970-01-01"}}, where: {id: {_eq: $callback_id} }){
|
||||
cwd
|
||||
impersonation_context
|
||||
extra_info
|
||||
host
|
||||
pid
|
||||
process_short_name
|
||||
user
|
||||
ip
|
||||
color
|
||||
integrity_level
|
||||
architecture
|
||||
}
|
||||
}
|
||||
`;
|
||||
const GetUpDownArrowName = (task, useDisplayParamsForCLIHistoryUserSetting) => {
|
||||
if(task.command){
|
||||
if(task?.command?.payloadtype?.use_display_params_for_cli_history){
|
||||
@@ -153,10 +172,24 @@ const IsRepeatableCLIParameterType = (parameter_type) => {
|
||||
|
||||
export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
const toastId = "tasking-toast-message";
|
||||
const theme = useTheme();
|
||||
const inputRef = React.useRef(null);
|
||||
const snackMessageStyles = {position:"bottom-left", autoClose: 1000, toastId: toastId, style: {marginBottom: "30px"}};
|
||||
const snackReverseSearchMessageStyles = {position:"bottom-left", autoClose: 1000, toastId: toastId, style: {marginBottom: "70px"}};
|
||||
const [commandPayloadType, setCommandPayloadType] = React.useState("");
|
||||
const [callbackContext, setCallbackContext] = React.useState({
|
||||
cwd: "",
|
||||
impersonation_context: "",
|
||||
extra_info: "",
|
||||
ip: "",
|
||||
host: "",
|
||||
user: "",
|
||||
pid: "",
|
||||
process_short_name: "",
|
||||
color: "",
|
||||
integrity_level: 2,
|
||||
architecture: "",
|
||||
});
|
||||
const [message, setMessage] = React.useState("");
|
||||
const loadedOptions = React.useRef([]);
|
||||
const taskOptions = React.useRef([]);
|
||||
@@ -229,7 +262,10 @@ export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
const commandOptionsForcePopup = React.useRef(false);
|
||||
const [openSelectCommandDialog, setOpenSelectCommandDialog] = React.useState(false);
|
||||
const me = useReactiveVar(meState);
|
||||
const useDisplayParamsForCLIHistoryUserSetting = React.useRef(GetMythicSetting({setting_name: "useDisplayParamsForCLIHistory", default_value: "true"}));
|
||||
const useDisplayParamsForCLIHistoryUserSetting = React.useRef(GetMythicSetting({setting_name: "useDisplayParamsForCLIHistory", default_value: operatorSettingDefaults.useDisplayParamsForCLIHistory}));
|
||||
const hideTaskingContext = React.useRef(GetMythicSetting({setting_name: "hideTaskingContext", default_value: operatorSettingDefaults.hideTaskingContext}));
|
||||
hideTaskingContext.current = props.hide_context | hideTaskingContext.current;
|
||||
const taskingContextFields = React.useRef(GetMythicSetting({setting_name: "taskingContextFields", default_value: operatorSettingDefaults.taskingContextFields}));
|
||||
const forwardOrBackwardTabIndex = (event, currentIndex, options) => {
|
||||
if(event.shiftKey){
|
||||
let newIndex = currentIndex - 1;
|
||||
@@ -249,8 +285,28 @@ export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
return;
|
||||
}
|
||||
tokenOptions.current = data.data.callbacktoken;
|
||||
if(tokenOptions.current.length === 0) {
|
||||
props.changeSelectedToken("Default Token");
|
||||
}
|
||||
}
|
||||
});
|
||||
useSubscription(contextSubscription, {
|
||||
variables: {callback_id: props.callback_id}, fetchPolicy: "network-only",
|
||||
shouldResubscribe: true,
|
||||
onData: ({data}) => {
|
||||
if(!mountedRef.current || !props.parentMountedRef.current){
|
||||
return;
|
||||
}
|
||||
let newContext = data.data.callback_stream[0];
|
||||
try{
|
||||
let ips = JSON.parse(newContext.ip);
|
||||
newContext.ip = ips[0];
|
||||
}catch(error){
|
||||
newContext.ip = "";
|
||||
}
|
||||
setCallbackContext(newContext);
|
||||
}
|
||||
});
|
||||
useSubscription(subscriptionTask, {
|
||||
variables: {callback_id: props.callback_id}, fetchPolicy: "network-only",
|
||||
shouldResubscribe: true,
|
||||
@@ -737,13 +793,13 @@ export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
str.split('').forEach((value, i, s) => {
|
||||
//loop over every value in the string
|
||||
//console.log(value);
|
||||
if(value === "\\"){
|
||||
if( (sQuoted || dQuoted) && value === "\\"){
|
||||
if(!backslash){
|
||||
backslash = true;
|
||||
return;
|
||||
} else {
|
||||
backslash = false;
|
||||
buffer += "\\\\"
|
||||
buffer += "\\"
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -771,7 +827,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
buffer += value;
|
||||
return;
|
||||
}
|
||||
else if(value === " "){
|
||||
else if([" ", "\t", "\r", "\n"].includes(value)){
|
||||
if(backslash){
|
||||
backslash = false;
|
||||
//buffer += " ";
|
||||
@@ -1357,22 +1413,22 @@ export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
let parsedWithPositionalParameters = {};
|
||||
let params = splitMessage.slice(1).join(" ");
|
||||
let failed_json_parse = true;
|
||||
if(unmodifiedHistoryValue === "modal" || unmodifiedHistoryValue.includes("browserscript")){
|
||||
// these are the two kinds that'll introduce dictionary values as original_params
|
||||
try{
|
||||
parsedWithPositionalParameters = JSON.parse(params);
|
||||
cmdGroupName = determineCommandGroupName(cmd, parsedWithPositionalParameters);
|
||||
if(cmdGroupName !== undefined){
|
||||
cmdGroupName.sort()
|
||||
} else {
|
||||
snackActions.warning("Two or more of the specified parameters can't be used together", snackMessageStyles);
|
||||
return;
|
||||
}
|
||||
failed_json_parse = false;
|
||||
|
||||
}catch(error){
|
||||
failed_json_parse = true;
|
||||
try{
|
||||
parsedWithPositionalParameters = JSON.parse(params);
|
||||
if(['string', 'number', 'boolean', null].includes(typeof parsedWithPositionalParameters)){
|
||||
throw("failed to parse json");
|
||||
}
|
||||
cmdGroupName = determineCommandGroupName(cmd, parsedWithPositionalParameters);
|
||||
if(cmdGroupName !== undefined){
|
||||
cmdGroupName.sort()
|
||||
} else {
|
||||
snackActions.warning("Two or more of the specified parameters can't be used together", snackMessageStyles);
|
||||
return;
|
||||
}
|
||||
failed_json_parse = false;
|
||||
|
||||
}catch(error){
|
||||
failed_json_parse = true;
|
||||
}
|
||||
if(failed_json_parse){
|
||||
let parsed = parseCommandLine(params, cmd);
|
||||
@@ -1393,7 +1449,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
if(cmd.commandparameters.length > 0){
|
||||
parsed["_"].unshift(cmd);
|
||||
parsedWithPositionalParameters = fillOutPositionalArguments(cmd, parsed, cmdGroupName);
|
||||
//console.log(parsedWithPositionalParameters);
|
||||
console.log("what's left", parsedWithPositionalParameters);
|
||||
if(parsedWithPositionalParameters === undefined){
|
||||
return;
|
||||
}
|
||||
@@ -1417,8 +1473,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
}else{
|
||||
let simplifiedGroupName = simplifyGroupNameChoices(cmdGroupName, cmd, parsedWithPositionalParameters)
|
||||
if(simplifiedGroupName === "" ){
|
||||
snackActions.warning("Passed arguments are ambiguous, use shift+enter for modal or provide more parameters", snackMessageStyles);
|
||||
return;
|
||||
props.onSubmitCommandLine(message, cmd, parsedWithPositionalParameters, true, cmdGroupName, unmodifiedHistoryValue)
|
||||
} else {
|
||||
props.onSubmitCommandLine(message, cmd, parsedWithPositionalParameters, Boolean(commandOptionsForcePopup.current), [simplifiedGroupName], unmodifiedHistoryValue);
|
||||
}
|
||||
@@ -1434,6 +1489,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
}
|
||||
console.log("positional args added in:", parsedWithPositionalParameters);
|
||||
console.log("about to call onSubmitCommandLine", cmd);
|
||||
console.log("commandOptionsForcePopup", Boolean(commandOptionsForcePopup.current), "group name", cmdGroupName)
|
||||
props.onSubmitCommandLine(message, cmd, parsedWithPositionalParameters, Boolean(commandOptionsForcePopup.current), cmdGroupName, unmodifiedHistoryValue);
|
||||
setMessage("");
|
||||
setCommandPayloadType("");
|
||||
@@ -1554,25 +1610,118 @@ export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
return (
|
||||
<div style={{position: "relative"}}>
|
||||
{backdropOpen && <Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={false}>
|
||||
<CircularProgress color="inherit" size={30} />
|
||||
<CircularProgress color="inherit" size={30}/>
|
||||
</Backdrop>
|
||||
}
|
||||
{reverseSearching &&
|
||||
<TextField
|
||||
placeholder={"Search previous commands"}
|
||||
onKeyDown={onReverseSearchKeyDown}
|
||||
onChange={handleReverseSearchInputChange}
|
||||
onKeyDown={onReverseSearchKeyDown}
|
||||
onChange={handleReverseSearchInputChange}
|
||||
size="small"
|
||||
color={"secondary"}
|
||||
autoFocus={true}
|
||||
variant="outlined"
|
||||
value={reverseSearchString}
|
||||
fullWidth={true}
|
||||
InputProps={{ type: 'search',
|
||||
startAdornment: <React.Fragment><Typography style={{width: "10%"}}>reverse-i-search:</Typography></React.Fragment>
|
||||
InputProps={{
|
||||
type: 'search',
|
||||
startAdornment: <React.Fragment><Typography
|
||||
style={{width: "10%"}}>reverse-i-search:</Typography></React.Fragment>
|
||||
}}
|
||||
/>
|
||||
}
|
||||
{callbackContext?.impersonation_context !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("impersonation_context") &&
|
||||
<MythicStyledTooltip title={"Impersonation Context"}>
|
||||
<span className={"rounded-tab"} style={{
|
||||
backgroundColor: theme.taskContextImpersonationColor,
|
||||
borderColor: callbackContext.color === "" ? "" : callbackContext.color
|
||||
}}>
|
||||
<b>{"User: "}</b>{callbackContext.impersonation_context}
|
||||
</span>
|
||||
</MythicStyledTooltip>
|
||||
|
||||
}
|
||||
{callbackContext?.user !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("user") &&
|
||||
<MythicStyledTooltip title={"User Context" + (callbackContext.integrity_level > 2 ? " (high integrity)" : "")}>
|
||||
<span className={"rounded-tab"} style={{
|
||||
backgroundColor: theme.taskContextColor,
|
||||
borderColor: callbackContext.color === "" ? "" : callbackContext.color
|
||||
}}>
|
||||
<b>{"User: "}</b>{callbackContext.user}{callbackContext.integrity_level > 2 ? "*" : ""}
|
||||
</span>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
{callbackContext?.cwd !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("cwd") &&
|
||||
<MythicStyledTooltip title={"Current Working Directory"}>
|
||||
<span className={"rounded-tab"} style={{
|
||||
backgroundColor: theme.taskContextColor,
|
||||
borderColor: callbackContext.color === "" ? "" : callbackContext.color
|
||||
}}>
|
||||
<b>{"Dir: "}</b>{callbackContext.cwd}
|
||||
</span>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
{callbackContext?.host !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("host") &&
|
||||
<MythicStyledTooltip title={"Hostname"}>
|
||||
<span className={"rounded-tab"} style={{
|
||||
backgroundColor: theme.taskContextColor,
|
||||
borderColor: callbackContext.color === "" ? "" : callbackContext.color
|
||||
}}>
|
||||
<b>{"Host: "}</b>{callbackContext.host}
|
||||
</span>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
{callbackContext?.ip !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("ip") &&
|
||||
<MythicStyledTooltip title={"First IP Address"}>
|
||||
<span className={"rounded-tab"} style={{
|
||||
backgroundColor: theme.taskContextColor,
|
||||
borderColor: callbackContext.color === "" ? "" : callbackContext.color
|
||||
}}>
|
||||
<b>{"IP: "}</b>{callbackContext.ip}
|
||||
</span>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
{callbackContext?.pid !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("pid") &&
|
||||
<MythicStyledTooltip title={"Process ID"}>
|
||||
<span className={"rounded-tab"} style={{
|
||||
backgroundColor: theme.taskContextColor,
|
||||
borderColor: callbackContext.color === "" ? "" : callbackContext.color
|
||||
}}>
|
||||
<b>{"PID: "}</b>{callbackContext.pid}
|
||||
</span>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
{callbackContext?.architecture !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("architecture") &&
|
||||
<MythicStyledTooltip title={"Process Architecture"}>
|
||||
<span className={"rounded-tab"} style={{
|
||||
backgroundColor: theme.taskContextColor,
|
||||
borderColor: callbackContext.color === "" ? "" : callbackContext.color
|
||||
}}>
|
||||
<b>{"Arch: "}</b>{callbackContext.architecture}
|
||||
</span>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
{callbackContext?.process_short_name !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("process_short_name") &&
|
||||
<MythicStyledTooltip title={"Process Name"}>
|
||||
<span className={"rounded-tab"} style={{
|
||||
backgroundColor: theme.taskContextColor,
|
||||
borderColor: callbackContext.color === "" ? "" : callbackContext.color
|
||||
}}>
|
||||
<b>{"Process: "}</b>{callbackContext.process_short_name}
|
||||
</span>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
{callbackContext?.extra_info !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("extra_info") &&
|
||||
<MythicStyledTooltip title={"Extra Callback Context"}>
|
||||
<span className={"rounded-tab"} style={{
|
||||
backgroundColor: theme.taskContextExtraColor,
|
||||
borderColor: callbackContext.color === "" ? "" : callbackContext.color
|
||||
}}>
|
||||
{callbackContext.extra_info}
|
||||
</span>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
<TextField
|
||||
placeholder={"Task an agent..."}
|
||||
onKeyDown={onKeyDown}
|
||||
@@ -1594,55 +1743,70 @@ export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
autoFocus: true,
|
||||
style: {paddingTop: "0px", paddingBottom: "0px", paddingRight: "5px"},
|
||||
endAdornment:
|
||||
<React.Fragment>
|
||||
<IconButton
|
||||
color="info"
|
||||
variant="contained"
|
||||
disableRipple={true}
|
||||
disableFocusRipple={true}
|
||||
onClick={onSubmitCommandLine}
|
||||
size="large"><SendIcon/>
|
||||
</IconButton>
|
||||
{props.filterTasks &&
|
||||
<React.Fragment>
|
||||
<IconButton
|
||||
color={activeFiltering ? "warning" : "secondary"}
|
||||
color="info"
|
||||
variant="contained"
|
||||
onClick={onClickFilter}
|
||||
style={{paddingLeft: 0}}
|
||||
disableRipple={true}
|
||||
disableFocusRipple={true}
|
||||
size="large"><TuneIcon/></IconButton>
|
||||
}
|
||||
{commandPayloadType !== "" &&
|
||||
<MythicAgentSVGIcon payload_type={commandPayloadType}
|
||||
style={{width: "35px", height: "35px"}}/>
|
||||
}
|
||||
</React.Fragment>
|
||||
onClick={onSubmitCommandLine}
|
||||
size="large"><SendIcon/>
|
||||
</IconButton>
|
||||
{props.filterTasks &&
|
||||
<IconButton
|
||||
color={activeFiltering ? "warning" : "secondary"}
|
||||
variant="contained"
|
||||
onClick={onClickFilter}
|
||||
style={{paddingLeft: 0}}
|
||||
disableRipple={true}
|
||||
disableFocusRipple={true}
|
||||
size="large"><TuneIcon/></IconButton>
|
||||
}
|
||||
{commandPayloadType !== "" &&
|
||||
<MythicAgentSVGIcon payload_type={commandPayloadType}
|
||||
style={{width: "35px", height: "35px"}}/>
|
||||
}
|
||||
</React.Fragment>
|
||||
,
|
||||
startAdornment: <React.Fragment>
|
||||
{tokenOptions.current.length > 0 ? (
|
||||
<CallbacksTabsTaskingInputTokenSelect options={tokenOptions.current} changeSelectedToken={props.changeSelectedToken}/>
|
||||
<CallbacksTabsTaskingInputTokenSelect options={tokenOptions.current}
|
||||
changeSelectedToken={props.changeSelectedToken}/>
|
||||
) : null}
|
||||
|
||||
</React.Fragment>
|
||||
|
||||
}}
|
||||
/>
|
||||
{openFilterOptionsDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openFilterOptionsDialog}
|
||||
onClose={() => {
|
||||
setOpenFilterOptionsDialog(false);
|
||||
}}
|
||||
innerDialog={<CallbacksTabsTaskingFilterDialog
|
||||
filterCommandOptions={loadedOptions.current} onSubmit={props.onSubmitFilter}
|
||||
filterOptions={props.filterOptions} onClose={() => {
|
||||
setOpenFilterOptionsDialog(false);
|
||||
}}/>}
|
||||
/>
|
||||
{openFilterOptionsDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openFilterOptionsDialog}
|
||||
onClose={()=>{setOpenFilterOptionsDialog(false);}}
|
||||
innerDialog={<CallbacksTabsTaskingFilterDialog filterCommandOptions={loadedOptions.current} onSubmit={props.onSubmitFilter} filterOptions={props.filterOptions} onClose={()=>{setOpenFilterOptionsDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
}
|
||||
{openSelectCommandDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openSelectCommandDialog}
|
||||
onClose={()=>{setOpenSelectCommandDialog(false)}}
|
||||
innerDialog={<MythicSelectFromListDialog onClose={()=>{setOpenSelectCommandDialog(false);}}
|
||||
onSubmit={processCommandAndCommandLine} options={commandOptions.current} title={"Select Command"}
|
||||
action={"select"} identifier={"id"} display={"display"} />}
|
||||
onClose={() => {
|
||||
setOpenSelectCommandDialog(false)
|
||||
}}
|
||||
innerDialog={<MythicSelectFromListDialog onClose={() => {
|
||||
setOpenSelectCommandDialog(false);
|
||||
}}
|
||||
onSubmit={processCommandAndCommandLine}
|
||||
options={commandOptions.current}
|
||||
title={"Select Command"}
|
||||
action={"select"} identifier={"id"}
|
||||
display={"display"}/>}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const CallbacksTabsTaskingInput = React.memo(CallbacksTabsTaskingInputPreMemo);
|
||||
+1
-1
@@ -42,7 +42,7 @@ export function CallbacksTabsTaskingInputTokenSelect(props) {
|
||||
}
|
||||
}
|
||||
return (
|
||||
<FormControl style={{width: "20%"}}>
|
||||
<FormControl style={{width: props.width ? props.width : "20%"}}>
|
||||
<Select
|
||||
labelId="demo-dialog-select-label"
|
||||
id="demo-dialog-select"
|
||||
|
||||
@@ -50,7 +50,7 @@ export function CallbacksTabsTaskingSplitLabel(props){
|
||||
]);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicTabLabel label={description} onDragTab={props.onDragTab} me={props.me} {...props} contextMenuOptions={contextMenuOptions}/>
|
||||
<MythicTabLabel label={description} highlight={props.newDataForTab[props.tabInfo.tabID]} onDragTab={props.onDragTab} me={props.me} {...props} contextMenuOptions={contextMenuOptions}/>
|
||||
{openEditDescriptionDialog &&
|
||||
<MythicDialog fullWidth={true} open={openEditDescriptionDialog} onClose={() => {setOpenEditDescriptionDialog(false);}}
|
||||
innerDialog={
|
||||
@@ -82,7 +82,7 @@ query getBatchTasking($callback_id: Int!, $offset: Int!, $fetchLimit: Int!){
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTab, me}) =>{
|
||||
export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTab, me, setNewDataForTab}) =>{
|
||||
const [taskLimit, setTaskLimit] = React.useState(30);
|
||||
const [scrollToBottom, setScrollToBottom] = React.useState(false);
|
||||
const [openParametersDialog, setOpenParametersDialog] = React.useState(false);
|
||||
@@ -171,6 +171,9 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
|
||||
if(!fetched){
|
||||
setFetched(true);
|
||||
}
|
||||
if(index !== value && fetched){
|
||||
setNewDataForTab((prev) => {return {...prev, [tabInfo.tabID]: true}});
|
||||
}
|
||||
//console.log("new subscription data in CallbacksTabsTasking", subscriptionData);
|
||||
const oldLength = taskingDataRef.current.task.length;
|
||||
const mergedData = data.data.task.reduce( (prev, cur) => {
|
||||
@@ -374,7 +377,7 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
|
||||
}
|
||||
}
|
||||
const changeSelectedTask = (task) => {
|
||||
let updated = {...taskingData};
|
||||
let updated = {...taskingDataRef.current};
|
||||
for(let i = 0; i < updated.task.length; i++){
|
||||
if(updated.task[i].id === task.id){
|
||||
updated.task[i].selected = true;
|
||||
@@ -411,7 +414,8 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
|
||||
}, [taskingData, newlyIssuedTasks]);
|
||||
return (
|
||||
<MythicTabPanel index={index} value={value} >
|
||||
<Split direction="horizontal" minSize={[0,0]} style={{width: "100%", height: "100%", overflowY: "auto", display: "flex"}} sizes={[30, 70]} >
|
||||
<Split direction="horizontal" minSize={[0,0]} sizes={[30, 70]}
|
||||
style={{width: "100%", height: "100%", overflow: "auto", display: "flex", justifyContent: "flex-end"}} >
|
||||
<div className="bg-gray-base" style={{display: "inline-flex", flexDirection: "column"}} >
|
||||
|
||||
<div style={{overflowY: "auto", flexGrow: 1}} id={`taskingPanelSplit${tabInfo.callbackID}`}>
|
||||
@@ -443,7 +447,14 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="bg-gray-light" style={{display: "inline-flex", height: "100%"}}>
|
||||
<div className="bg-gray-light" style={{display: "inline-flex", flexDirection: "column", height: "100%", width: "100%"}}>
|
||||
{selectedTask.id > 0 && <TaskDisplayFlat key={"taskinteractdisplaysplit" + selectedTask.id} me={me} task={selectedTask}
|
||||
command_id={selectedTask.command == null ? 0 : selectedTask.command.id}
|
||||
filterOptions={filterOptions}
|
||||
onSelectTask={(tsk) => {
|
||||
}}
|
||||
selectedTask={selectedTask}
|
||||
/>}
|
||||
<CallbacksTabsTaskingSplitTable selectedTask={selectedTask} me={me} filterOptions={filterOptions}
|
||||
onSelectTask={(tsk) => {changeSelectedTask(tsk)}} changeSelectedTask={changeSelectedTask}
|
||||
/>
|
||||
|
||||
@@ -13,6 +13,8 @@ const SUB_Callbacks = gql`
|
||||
subscription CallbacksSubscription($fromNow: timestamp!){
|
||||
callback_stream(batch_size: 1000, cursor: {initial_value: {timestamp: $fromNow}}) {
|
||||
architecture
|
||||
cwd
|
||||
impersonation_context
|
||||
active
|
||||
dead
|
||||
color
|
||||
@@ -41,6 +43,14 @@ callback_stream(batch_size: 1000, cursor: {initial_value: {timestamp: $fromNow}}
|
||||
current_time
|
||||
mythictree_groups
|
||||
trigger_on_checkin_after_time
|
||||
tags {
|
||||
tagtype {
|
||||
name
|
||||
color
|
||||
id
|
||||
}
|
||||
id
|
||||
}
|
||||
callbackports(where: {deleted: {_eq: false}}) {
|
||||
local_port
|
||||
remote_port
|
||||
@@ -67,6 +77,8 @@ export const Query_Callbacks_And_Edges = gql`
|
||||
query InitialActiveCallbacksQuery{
|
||||
callback(where: {active: {_eq: true}}) {
|
||||
architecture
|
||||
cwd
|
||||
impersonation_context
|
||||
active
|
||||
dead
|
||||
color
|
||||
@@ -95,6 +107,14 @@ callback(where: {active: {_eq: true}}) {
|
||||
current_time
|
||||
mythictree_groups
|
||||
trigger_on_checkin_after_time
|
||||
tags {
|
||||
tagtype {
|
||||
name
|
||||
color
|
||||
id
|
||||
}
|
||||
id
|
||||
}
|
||||
callbackports(where: {deleted: {_eq: false}}) {
|
||||
id
|
||||
local_port
|
||||
@@ -131,6 +151,14 @@ callback(where: {active: {_eq: true}}) {
|
||||
process_name
|
||||
integrity_level
|
||||
extra_info
|
||||
tags {
|
||||
tagtype {
|
||||
name
|
||||
color
|
||||
id
|
||||
}
|
||||
id
|
||||
}
|
||||
payload {
|
||||
payloadtype {
|
||||
name
|
||||
@@ -140,6 +168,8 @@ callback(where: {active: {_eq: true}}) {
|
||||
callbackc2profiles {
|
||||
c2profile {
|
||||
name
|
||||
has_logo
|
||||
is_p2p
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,6 +186,14 @@ callback(where: {active: {_eq: true}}) {
|
||||
process_name
|
||||
integrity_level
|
||||
extra_info
|
||||
tags {
|
||||
tagtype {
|
||||
name
|
||||
color
|
||||
id
|
||||
}
|
||||
id
|
||||
}
|
||||
payload {
|
||||
payloadtype {
|
||||
name
|
||||
@@ -165,6 +203,8 @@ callback(where: {active: {_eq: true}}) {
|
||||
callbackc2profiles {
|
||||
c2profile {
|
||||
name
|
||||
has_logo
|
||||
is_p2p
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,6 +212,7 @@ callback(where: {active: {_eq: true}}) {
|
||||
id
|
||||
is_p2p
|
||||
name
|
||||
has_logo
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,6 +236,14 @@ callbackgraphedge_stream(batch_size: 100, cursor: {initial_value: {updated_at: $
|
||||
process_name
|
||||
integrity_level
|
||||
extra_info
|
||||
tags {
|
||||
tagtype {
|
||||
name
|
||||
color
|
||||
id
|
||||
}
|
||||
id
|
||||
}
|
||||
payload {
|
||||
payloadtype {
|
||||
name
|
||||
@@ -204,6 +253,8 @@ callbackgraphedge_stream(batch_size: 100, cursor: {initial_value: {updated_at: $
|
||||
callbackc2profiles {
|
||||
c2profile {
|
||||
name
|
||||
has_logo
|
||||
is_p2p
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,6 +272,14 @@ callbackgraphedge_stream(batch_size: 100, cursor: {initial_value: {updated_at: $
|
||||
process_name
|
||||
integrity_level
|
||||
extra_info
|
||||
tags {
|
||||
tagtype {
|
||||
name
|
||||
color
|
||||
id
|
||||
}
|
||||
id
|
||||
}
|
||||
payload {
|
||||
payloadtype {
|
||||
name
|
||||
@@ -230,6 +289,8 @@ callbackgraphedge_stream(batch_size: 100, cursor: {initial_value: {updated_at: $
|
||||
callbackc2profiles {
|
||||
c2profile {
|
||||
name
|
||||
has_logo
|
||||
is_p2p
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -237,6 +298,7 @@ callbackgraphedge_stream(batch_size: 100, cursor: {initial_value: {updated_at: $
|
||||
id
|
||||
is_p2p
|
||||
name
|
||||
has_logo
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -322,7 +384,7 @@ export function CallbacksTop(props){
|
||||
callbackEdges.current = updated;
|
||||
}
|
||||
});
|
||||
const onOpenTabLocal = React.useCallback( ({tabType, tabID, callbackID}) => {
|
||||
const onOpenTabLocal = React.useCallback( ({tabType, tabID, callbackID, customBrowser}) => {
|
||||
for(let i = 0; i < callbacks.current.length; i++){
|
||||
if(callbacks.current[i]["id"] === callbackID){
|
||||
const tabData = {tabID, tabType, callbackID,
|
||||
@@ -334,7 +396,9 @@ export function CallbacksTop(props){
|
||||
callbackDescription: callbacks.current[i]["description"],
|
||||
host: callbacks.current[i]["host"],
|
||||
color: callbacks.current[i]["color"],
|
||||
os: callbacks.current[i]["payload"]["os"]};
|
||||
os: callbacks.current[i]["payload"]["os"],
|
||||
customBrowser: customBrowser,
|
||||
};
|
||||
props.onOpenTab(tabData);
|
||||
}
|
||||
}
|
||||
@@ -356,7 +420,9 @@ export function CallbacksTop(props){
|
||||
callbackDescription: callbacks.current[i]["description"],
|
||||
host: callbacks.current[i]["host"],
|
||||
color: callbacks.current[i]["color"],
|
||||
os: callbacks.current[i]["payload"]["os"]};
|
||||
os: callbacks.current[i]["payload"]["os"],
|
||||
customBrowser: tabs[j].customBrowser,
|
||||
};
|
||||
newTabData.push(tabData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,14 +31,25 @@ import InfoIconOutline from '@mui/icons-material/InfoOutlined';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import {HostFileDialog} from "../Payloads/HostFileDialog";
|
||||
import PublicIcon from '@mui/icons-material/Public';
|
||||
import {payloadsCallbackAllowed} from "../Payloads/Payloads";
|
||||
import Switch from '@mui/material/Switch';
|
||||
|
||||
const GET_Payload_Details = gql`
|
||||
query GetCallbackDetails($callback_id: Int!) {
|
||||
callback_by_pk(id: $callback_id){
|
||||
tags {
|
||||
tagtype {
|
||||
name
|
||||
color
|
||||
id
|
||||
}
|
||||
id
|
||||
}
|
||||
payload {
|
||||
uuid
|
||||
id
|
||||
creation_time
|
||||
callback_allowed
|
||||
payloadtype{
|
||||
name
|
||||
agent_type
|
||||
@@ -145,6 +156,8 @@ query GetCallbackDetails($callback_id: Int!) {
|
||||
process_name
|
||||
init_callback
|
||||
mythictree_groups
|
||||
cwd
|
||||
impersonation_context
|
||||
}
|
||||
|
||||
}
|
||||
@@ -210,6 +223,7 @@ export function DetailedCallbackTable(props){
|
||||
issueNextMod();
|
||||
}
|
||||
})
|
||||
const [payloadCallbackAllowed, setPayloadCallbackAllowed] = React.useState(true);
|
||||
const issueNextMod = () => {
|
||||
if(commandMods.current.add >= addTotal.current){
|
||||
if(commandMods.current.remove >= removeTotal.current) {
|
||||
@@ -286,8 +300,29 @@ export function DetailedCallbackTable(props){
|
||||
}];
|
||||
}, []);
|
||||
setC2Profiles(c2ProfilesState);
|
||||
setPayloadCallbackAllowed(data.callback_by_pk.payload.callback_allowed);
|
||||
}
|
||||
});
|
||||
const [toggleCallbackAllowedMutation] = useMutation(payloadsCallbackAllowed, {
|
||||
onCompleted: (data) => {
|
||||
if(data.updatePayload.status === "success"){
|
||||
setPayloadCallbackAllowed(data.updatePayload.callback_allowed);
|
||||
if(data.updatePayload.callback_allowed){
|
||||
snackActions.success("New Callbacks allowed from this Payload");
|
||||
} else {
|
||||
snackActions.warning("No new Callback allowed from this Payload");
|
||||
}
|
||||
} else {
|
||||
console.log(data.updatePayload);
|
||||
}
|
||||
},
|
||||
onError: (data) => {
|
||||
console.log(data);
|
||||
}
|
||||
});
|
||||
const onToggleCallbackAllowed = () => {
|
||||
toggleCallbackAllowedMutation({variables: {payload_uuid: data.callback_by_pk.payload.uuid, callback_allowed: !payloadCallbackAllowed}})
|
||||
}
|
||||
if (loading) {
|
||||
return <LinearProgress style={{marginTop: "10px"}}/>;
|
||||
}
|
||||
@@ -384,6 +419,18 @@ export function DetailedCallbackTable(props){
|
||||
<TableCell>Created By</TableCell>
|
||||
<TableCell>{data.callback_by_pk.payload?.operator?.username}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>New Callbacks Allowed?</TableCell>
|
||||
<TableCell>
|
||||
<Switch
|
||||
checked={payloadCallbackAllowed}
|
||||
onChange={onToggleCallbackAllowed}
|
||||
color="info"
|
||||
inputProps={{'aria-label': 'primary checkbox'}}
|
||||
name="callback_allowed"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{data.callback_by_pk.payload?.eventstepinstance &&
|
||||
<>
|
||||
<TableRow hover>
|
||||
|
||||
@@ -39,8 +39,8 @@ export function DownloadHistoryDialog(props){
|
||||
<TableRow>
|
||||
<TableCell style={{}}></TableCell>
|
||||
<TableCell style={{width: "15rem"}}>Time</TableCell>
|
||||
<TableCell style={{width: "5rem"}}>Task</TableCell>
|
||||
<TableCell style={{width: "5rem"}}>Callback</TableCell>
|
||||
<TableCell style={{width: "7rem"}}>Task</TableCell>
|
||||
<TableCell style={{width: "7rem"}}>Callback</TableCell>
|
||||
<TableCell>Comment</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
@@ -16,6 +16,8 @@ import {ResponseDisplayInteractive} from "./ResponseDisplayInteractive";
|
||||
import {ResponseDisplayMedia} from "./ResponseDisplayMedia";
|
||||
import {GetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {ResponseDisplayGraph} from "./ResponseDisplayGraph";
|
||||
import {operatorSettingDefaults} from "../../../cache";
|
||||
import {ResponseDisplayTabs} from "./ResponseDisplayTabs";
|
||||
|
||||
const subResponsesStream = gql`
|
||||
subscription subResponsesStream($task_id: Int!){
|
||||
@@ -95,13 +97,14 @@ export const ResponseDisplay = (props) =>{
|
||||
const interactive = props?.task?.command?.supported_ui_features.includes("task_response:interactive") || false;
|
||||
return (
|
||||
interactive ? (
|
||||
<ResponseDisplayInteractive {...props} />
|
||||
<ResponseDisplayInteractive {...props} key={props?.task?.id} />
|
||||
) : (
|
||||
<NonInteractiveResponseDisplay {...props} />
|
||||
<NonInteractiveResponseDisplay {...props} key={props?.task?.id} />
|
||||
)
|
||||
)
|
||||
}
|
||||
const NonInteractiveResponseDisplay = (props) => {
|
||||
const theme = useTheme();
|
||||
const [output, setOutput] = React.useState("");
|
||||
const [rawResponses, setRawResponses] = React.useState([]);
|
||||
const seenResponseIDs = React.useRef([]);
|
||||
@@ -109,7 +112,7 @@ const NonInteractiveResponseDisplay = (props) => {
|
||||
const [totalCount, setTotalCount] = React.useState(0);
|
||||
const [openBackdrop, setOpenBackdrop] = React.useState(true);
|
||||
const togglingAllOutputToPaginated = React.useRef(false);
|
||||
const initialResponseStreamLimit = GetMythicSetting({setting_name: "experiment-responseStreamLimit", default_value: 50});
|
||||
const initialResponseStreamLimit = GetMythicSetting({setting_name: "experiment-responseStreamLimit", default_value: operatorSettingDefaults["experiment-responseStreamLimit"]});
|
||||
const [fetchMoreResponses] = useLazyQuery(getResponsesLazyQuery, {
|
||||
fetchPolicy: "network-only",
|
||||
onCompleted: (data) => {
|
||||
@@ -289,16 +292,19 @@ const NonInteractiveResponseDisplay = (props) => {
|
||||
}, [search.current]);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<Backdrop open={openBackdrop} onClick={()=>{setOpenBackdrop(false);}} style={{zIndex: 2, position: "absolute"}}>
|
||||
<CircularProgress color="inherit" disableShrink />
|
||||
</Backdrop>
|
||||
{props.searchOutput &&
|
||||
<SearchBar onSubmitSearch={onSubmitSearch} />
|
||||
}
|
||||
{!openBackdrop &&
|
||||
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%"}}>
|
||||
|
||||
{!openBackdrop &&
|
||||
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%",
|
||||
backgroundColor: theme.outputBackgroundColor + (theme.palette.mode === 'dark' ? "D0" : "D0"),
|
||||
color: theme.outputTextColor,
|
||||
}}>
|
||||
{props.searchOutput &&
|
||||
<SearchBar onSubmitSearch={onSubmitSearch} />
|
||||
}
|
||||
<div style={{overflowY: "auto", flexGrow: 1, width: "100%", height: props.expand ? "100%": undefined, display: "flex", flexDirection: "column"}} ref={props.responseRef}>
|
||||
<ResponseDisplayComponent rawResponses={rawResponses} viewBrowserScript={props.viewBrowserScript}
|
||||
output={output} command_id={props.command_id} displayType={"accordion"}
|
||||
@@ -312,7 +318,7 @@ const NonInteractiveResponseDisplay = (props) => {
|
||||
</div>
|
||||
|
||||
}
|
||||
</React.Fragment>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export const ResponseDisplayConsole = (props) => {
|
||||
@@ -363,15 +369,9 @@ export const NonInteractiveResponseDisplayConsole = (props) => {
|
||||
fetchPolicy: "network_only",
|
||||
onData: subscriptionDataCallback
|
||||
});
|
||||
const onSubmitSearch = React.useCallback( (newSearch) => {
|
||||
snackActions.warning("Search not supported for console view");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{props.searchOutput &&
|
||||
<SearchBar onSubmitSearch={onSubmitSearch}/>
|
||||
}
|
||||
<div style={{overflowY: "auto", width: "100%", height: props.expand ? "100%" : undefined}}
|
||||
ref={props.responseRef}>
|
||||
<ResponseDisplayComponent rawResponses={rawResponses} viewBrowserScript={props.viewBrowserScript}
|
||||
@@ -408,7 +408,7 @@ export const PaginationBar = ({selectAllOutput, totalCount, onSubmitPageChange,
|
||||
}, [totalCount, maxCount, search, selectAllOutput]);
|
||||
const pageCount = Math.max(1, Math.ceil(localTotalCount / pageSize));
|
||||
// don't bother people with pagination information if they haven't even started paginating
|
||||
if(pageCount < 2){
|
||||
if(pageCount < 2 || pageCount === Infinity || isNaN(pageCount)){
|
||||
return (<div id={'scrolltotaskbottom' + task.id}></div>)
|
||||
}
|
||||
return (
|
||||
@@ -546,47 +546,64 @@ const ResponseDisplayComponent = ({rawResponses, viewBrowserScript, output, comm
|
||||
return null
|
||||
}
|
||||
return (
|
||||
localViewBrowserScript && Object.keys(browserScriptData).length > 0 ? (
|
||||
<React.Fragment>
|
||||
{browserScriptData?.screenshot?.map( (scr, index) => (
|
||||
<ResponseDisplayScreenshot key={"screenshot" + index + 'fortask' + task.id} task={task} {...scr}
|
||||
displayType={displayType} expand={expand} />
|
||||
))
|
||||
}
|
||||
{browserScriptData?.plaintext !== undefined &&
|
||||
<ResponseDisplayPlaintext plaintext={browserScriptData["plaintext"]} task={task}
|
||||
expand={expand} displayType={displayType} />
|
||||
}
|
||||
{browserScriptData?.table?.map( (table, index) => (
|
||||
|
||||
<ResponseDisplayTable callback_id={task.callback_id} task={task} expand={expand}
|
||||
table={table} key={"tablefortask" + task.id + "table" + index}
|
||||
displayType={displayType}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{browserScriptData?.download?.map( (dl, index) => (
|
||||
<ResponseDisplayDownload download={dl} task={task} displayType={displayType}
|
||||
key={"download" + index + "fortask" + task.id} />
|
||||
))
|
||||
}
|
||||
{browserScriptData?.search?.map( (s, index) => (
|
||||
<ResponseDisplaySearch search={s} task={task} displayType={displayType}
|
||||
key={"searchlink" + index + "fortask" + task.id} />
|
||||
))
|
||||
}
|
||||
{browserScriptData?.media?.map( (s, index) => (
|
||||
<ResponseDisplayMedia key={"searchmedia" + index + "fortask" + task.id}
|
||||
displayType={displayType}
|
||||
task={task} media={s} expand={expand} />
|
||||
))}
|
||||
{browserScriptData?.graph !== undefined &&
|
||||
<ResponseDisplayGraph graph={browserScriptData.graph} task={task}
|
||||
expand={expand} displayType={displayType} />
|
||||
}
|
||||
</React.Fragment>
|
||||
localViewBrowserScript ? (
|
||||
<ResponseDisplayBrowserScriptComponent expand={expand} displayType={displayType}
|
||||
task={task} browserScriptData={browserScriptData}
|
||||
output={output}/>
|
||||
) : (
|
||||
<ResponseDisplayPlaintext plaintext={output} task={task} expand={expand} displayType={displayType}/>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export function ResponseDisplayBrowserScriptComponent({output, browserScriptData, task, expand, displayType}) {
|
||||
return (
|
||||
<>
|
||||
{Object.keys(browserScriptData).length > 0 ? (
|
||||
<>
|
||||
{browserScriptData?.screenshot?.map( (scr, index) => (
|
||||
<ResponseDisplayScreenshot key={"screenshot" + index + 'fortask' + task.id} task={task} {...scr}
|
||||
displayType={displayType} expand={expand} />
|
||||
))
|
||||
}
|
||||
{browserScriptData?.plaintext !== undefined &&
|
||||
<ResponseDisplayPlaintext plaintext={browserScriptData["plaintext"]} task={task}
|
||||
expand={expand} displayType={displayType} />
|
||||
}
|
||||
{browserScriptData?.table?.map( (table, index) => (
|
||||
<ResponseDisplayTable callback_id={task.callback_id} task={task} expand={expand}
|
||||
table={table} key={"tablefortask" + task.id + "table" + index}
|
||||
displayType={displayType}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{browserScriptData?.download?.map( (dl, index) => (
|
||||
<ResponseDisplayDownload download={dl} task={task} displayType={displayType}
|
||||
key={"download" + index + "fortask" + task.id} />
|
||||
))
|
||||
}
|
||||
{browserScriptData?.search?.map( (s, index) => (
|
||||
<ResponseDisplaySearch search={s} task={task} displayType={displayType}
|
||||
key={"searchlink" + index + "fortask" + task.id} />
|
||||
))
|
||||
}
|
||||
{browserScriptData?.media?.map( (s, index) => (
|
||||
<ResponseDisplayMedia key={"searchmedia" + index + "fortask" + task.id}
|
||||
displayType={displayType}
|
||||
task={task} media={s} expand={expand} />
|
||||
))}
|
||||
{browserScriptData?.graph !== undefined &&
|
||||
<ResponseDisplayGraph graph={browserScriptData.graph} task={task}
|
||||
expand={expand} displayType={displayType} />
|
||||
}
|
||||
{browserScriptData.tabs && browserScriptData.tabs.length > 0 &&
|
||||
<ResponseDisplayTabs task={task} expand={expand} displayType={displayType} tabs={browserScriptData.tabs} output={output} />
|
||||
}
|
||||
</>
|
||||
) : (
|
||||
<ResponseDisplayPlaintext plaintext={output} task={task} expand={expand} displayType={displayType}/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import WrapTextIcon from '@mui/icons-material/WrapText';
|
||||
import { useReactiveVar } from '@apollo/client';
|
||||
import { meState } from '../../../cache';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import HeightIcon from '@mui/icons-material/Height';
|
||||
|
||||
const getInteractiveTaskingQuery = gql`
|
||||
${taskingDataFragment}
|
||||
@@ -168,7 +169,6 @@ export const GetOutputFormatAll = ({data, myTask, taskID, useASNIColor, message
|
||||
|
||||
}
|
||||
|
||||
|
||||
const InteractiveMessageTypes = [
|
||||
{"name": "None", "value": -1, "text": "None"},
|
||||
{"name": "Tab", "value": 13, "text": "^I"},
|
||||
@@ -202,8 +202,9 @@ const EnterOptions = [
|
||||
];
|
||||
export const ResponseDisplayInteractive = (props) =>{
|
||||
const me = useReactiveVar(meState);
|
||||
const theme = useTheme();
|
||||
const [backdropOpen, setBackdropOpen] = React.useState(false);
|
||||
const [scrollToBottom, setScrollToBottom] = React.useState(false);
|
||||
const [scrollToBottom, setScrollToBottom] = React.useState(true);
|
||||
const pageSize = React.useRef(100);
|
||||
const highestFetched = React.useRef(0);
|
||||
const [taskData, setTaskData] = React.useState([]);
|
||||
@@ -218,6 +219,7 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
const [useASNIColor, setUseANSIColor] = React.useState(true);
|
||||
const [showTaskStatus, setShowTaskStatus] = React.useState(true);
|
||||
const [wrapText, setWrapText] = React.useState(true);
|
||||
const [autoScroll, setAutoScroll] = React.useState(true);
|
||||
const {loading: loadingTasks} = useSubscription(getInteractiveTaskingQuery, {
|
||||
variables: {parent_task_id: props.task.id},
|
||||
onError: data => {
|
||||
@@ -279,8 +281,7 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
if(backdropOpen){
|
||||
setBackdropOpen(false);
|
||||
}
|
||||
|
||||
}, [highestFetched.current, rawResponses, props.task.id, backdropOpen, taskIDRef.current]);
|
||||
}, [highestFetched.current, rawResponses, props.task.id, backdropOpen, taskIDRef.current, autoScroll]);
|
||||
useSubscription(subResponsesQuery, {
|
||||
variables: {task_id: props.task.id},
|
||||
fetchPolicy: "no-cache",
|
||||
@@ -298,13 +299,14 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
if(page.current === currentPage){
|
||||
const pageCount = Math.max(1, Math.ceil(allData.length / pageSize.current));
|
||||
if(page.current === pageCount -1){
|
||||
console.log("updating pageSize");
|
||||
// we just streamed more data and we're on the latest page, increase pageSize
|
||||
//pageSize.current += Math.abs(allData.length - totalCount);
|
||||
currentPage += 1;
|
||||
}
|
||||
}
|
||||
setAllOutput(allData.slice((currentPage-1)*pageSize.current, currentPage*pageSize.current));
|
||||
if(props.selectAllOutput){
|
||||
setAllOutput(allData);
|
||||
} else {
|
||||
setAllOutput(allData.slice((currentPage-1)*pageSize.current, currentPage*pageSize.current));
|
||||
}
|
||||
setTotalCount(allData.length);
|
||||
}else{
|
||||
let allData = [...rawResponses, ...taskData];
|
||||
@@ -337,6 +339,9 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
React.useEffect( () => {
|
||||
onSubmitPageChange(1);
|
||||
}, [search]);
|
||||
React.useEffect( () => {
|
||||
onSubmitPageChange(1);
|
||||
}, [props.selectAllOutput]);
|
||||
const onSubmitSearch = React.useCallback( (newSearch) => {
|
||||
setSearch(newSearch);
|
||||
}, []);
|
||||
@@ -363,11 +368,14 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
const toggleWrapText = () => {
|
||||
setWrapText(!wrapText);
|
||||
}
|
||||
useEffect( () => {
|
||||
if(scrollToBottom){
|
||||
messagesEndRef.current.scrollIntoView();
|
||||
const toggleAutoScroll = () => {
|
||||
setAutoScroll(!autoScroll);
|
||||
}
|
||||
useEffect(() => {
|
||||
if(autoScroll){
|
||||
messagesEndRef?.current?.scrollIntoView({ behavior: "auto", block: "nearest" });
|
||||
}
|
||||
}, [scrollToBottom]);
|
||||
}, [props.responseRef?.current?.scrollHeight, autoScroll]);
|
||||
React.useEffect( () => {
|
||||
if(loadingTasks){
|
||||
setTaskData([]);
|
||||
@@ -384,8 +392,13 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
|
||||
<div style={{
|
||||
display: "flex", overflowY: "auto",
|
||||
position: "relative", height: props.expand ? "100%" : undefined, maxHeight: props.expand ? "100%" : "500px",
|
||||
flexDirection: "column"
|
||||
position: "relative",
|
||||
height: props.expand ? "100%" : undefined,
|
||||
maxHeight: props.expand ? "100%" : "500px",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
backgroundColor: theme.outputBackgroundColor + (theme.palette.mode === 'dark' ? "B0" : "E0"),
|
||||
color: theme.outputTextColor,
|
||||
}}>
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute",}} invisible={false}>
|
||||
<div style={{
|
||||
@@ -404,19 +417,17 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
{props.searchOutput &&
|
||||
<SearchBar onSubmitSearch={onSubmitSearch}/>
|
||||
}
|
||||
<div style={{overflowY: "auto", width: "100%", marginBottom: "5px",
|
||||
<div style={{overflowY: "auto", width: "100%", marginBottom: "5px", height: props.expand ? "100%": undefined,
|
||||
flexGrow: 1, paddingLeft: "10px", minHeight: "50px"}} ref={props.responseRef}
|
||||
id={`ptytask${props.task.id}`}>
|
||||
|
||||
<GetOutputFormatAll data={alloutput}
|
||||
myTask={props.task.operator.username === (me?.user?.username || "")}
|
||||
taskID={props.task.id}
|
||||
useASNIColor={useASNIColor}
|
||||
messagesEndRef={messagesEndRef}
|
||||
showTaskStatus={showTaskStatus}
|
||||
search={props.searchOutput ? search : undefined}
|
||||
wrapText={wrapText}/>
|
||||
|
||||
<GetOutputFormatAll data={alloutput}
|
||||
myTask={props.task.operator.username === (me?.user?.username || "")}
|
||||
taskID={props.task.id}
|
||||
useASNIColor={useASNIColor}
|
||||
messagesEndRef={messagesEndRef}
|
||||
showTaskStatus={showTaskStatus}
|
||||
search={props.searchOutput ? search : undefined}
|
||||
wrapText={wrapText}/>
|
||||
<div ref={messagesEndRef}/>
|
||||
</div>
|
||||
{!props.task?.is_interactive_task &&
|
||||
@@ -425,20 +436,22 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
useASNIColor={useASNIColor} toggleANSIColor={toggleANSIColor}
|
||||
showTaskStatus={showTaskStatus} toggleShowTaskStatus={toggleShowTaskStatus}
|
||||
wrapText={wrapText} toggleWrapText={toggleWrapText}
|
||||
autoScroll={autoScroll} toggleAutoScroll={toggleAutoScroll}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<InteractivePaginationBar totalCount={totalCount} currentPage={page.current}
|
||||
onSubmitPageChange={onSubmitPageChange} expand={props.expand}
|
||||
pageSize={pageSize.current}/>
|
||||
pageSize={pageSize.current} selectAllOutput={props.selectAllOutput}/>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
const InteractiveTaskingBar = ({
|
||||
task, taskData, useASNIColor, toggleANSIColor,
|
||||
showTaskStatus, toggleShowTaskStatus, wrapText, toggleWrapText
|
||||
showTaskStatus, toggleShowTaskStatus, wrapText, toggleWrapText,
|
||||
autoScroll, toggleAutoScroll
|
||||
}) => {
|
||||
const [inputText, setInputText] = React.useState("");
|
||||
const theme = useTheme();
|
||||
@@ -455,8 +468,8 @@ const InteractiveTaskingBar = ({
|
||||
});
|
||||
const [taskOptionsIndex, setTaskOptionsIndex] = React.useState(-1);
|
||||
const [taskOptions, setTaskOptions] = React.useState([]);
|
||||
const [selectedEnterOption, setSelectedEnterOption] = React.useState(EnterOptions[1]);
|
||||
const [selectedControlOption, setSelectedControlOption] = React.useState(InteractiveMessageTypes[0]);
|
||||
const [selectedEnterOption, setSelectedEnterOption] = React.useState(1);
|
||||
const [selectedControlOption, setSelectedControlOption] = React.useState(0);
|
||||
React.useEffect( () => {
|
||||
const newTaskOptions = taskData.filter( t => t.display_params.length > 1 && (t.interactive_task_type === 0 || t.interactive_task_type === 8));
|
||||
newTaskOptions.sort( (a,b) => a.id > b.id ? -1 : 1);
|
||||
@@ -506,23 +519,23 @@ const InteractiveTaskingBar = ({
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if(event.shiftKey){
|
||||
setInputText(inputText + selectedEnterOption.value);
|
||||
setInputText(inputText + EnterOptions[selectedEnterOption].value);
|
||||
return;
|
||||
}
|
||||
if(event.metaKey || event.ctrlKey){
|
||||
setInputText(inputText + selectedEnterOption.value);
|
||||
setInputText(inputText + EnterOptions[selectedEnterOption].value);
|
||||
return;
|
||||
}
|
||||
if(selectedControlOption.value > 0){
|
||||
let ctrlSequence = selectedControlOption.text;
|
||||
let enterOption = selectedEnterOption.value;
|
||||
if(InteractiveMessageTypes[selectedControlOption].value > 0){
|
||||
let ctrlSequence = InteractiveMessageTypes[selectedControlOption].text;
|
||||
let enterOption = EnterOptions[selectedEnterOption].value;
|
||||
let originalParams = inputText + ctrlSequence + enterOption;
|
||||
// if we're looking at a tab, never send enter along with it
|
||||
if (selectedControlOption.value === 8){
|
||||
if (InteractiveMessageTypes[selectedControlOption].value === 8){
|
||||
originalParams = inputText + ctrlSequence;
|
||||
enterOption = "";
|
||||
// if we're looking at escape, never send enter along with it
|
||||
} else if(selectedControlOption.value === 4){
|
||||
} else if(InteractiveMessageTypes[selectedControlOption].value === 4){
|
||||
originalParams = ctrlSequence + inputText;
|
||||
enterOption = "";
|
||||
}
|
||||
@@ -535,16 +548,16 @@ const InteractiveTaskingBar = ({
|
||||
parameter_group_name: "default",
|
||||
parent_task_id: task.id,
|
||||
is_interactive_task: true,
|
||||
interactive_task_type: selectedControlOption.value,
|
||||
interactive_task_type: InteractiveMessageTypes[selectedControlOption].value,
|
||||
}})
|
||||
}else {
|
||||
// no control option selected, just send data along as input
|
||||
createTask({variables: {
|
||||
callback_id: task.callback.display_id,
|
||||
command: task.command.cmd,
|
||||
params: inputText + selectedEnterOption.value,
|
||||
params: inputText + EnterOptions[selectedEnterOption].value,
|
||||
tasking_location: "command_line",
|
||||
original_params: inputText + selectedEnterOption.value,
|
||||
original_params: inputText + EnterOptions[selectedEnterOption].value,
|
||||
parameter_group_name: "default",
|
||||
parent_task_id: task.id,
|
||||
is_interactive_task: true,
|
||||
@@ -552,7 +565,7 @@ const InteractiveTaskingBar = ({
|
||||
}})
|
||||
}
|
||||
setInputText("");
|
||||
setSelectedControlOption(InteractiveMessageTypes[0]);
|
||||
setSelectedControlOption(0);
|
||||
setTaskOptionsIndex(-1);
|
||||
}
|
||||
const onChangeSelect = (event) => {
|
||||
@@ -569,20 +582,29 @@ const InteractiveTaskingBar = ({
|
||||
<div style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-end",
|
||||
backgroundColor: theme.palette.graphGroupRGBA,
|
||||
//backgroundColor: theme.outputBackgroundColor + (theme.palette.mode === 'dark' ? "F0" : "50"),
|
||||
color: theme.outputTextColor,
|
||||
paddingTop: "5px",
|
||||
border: "1px solid grey",
|
||||
borderRadius: "4px",
|
||||
width: "100%"}}>
|
||||
<FormControl style={{width: "10rem", marginTop: "2px"}} >
|
||||
<InputLabel id="control-label" style={{}}>Controls</InputLabel>
|
||||
<FormControl style={{width: "10rem", marginTop: "2px", color: theme.outputTextColor}} >
|
||||
<InputLabel id="control-label" style={{color: theme.outputTextColor}}>Controls</InputLabel>
|
||||
<Select
|
||||
labelId="control-label"
|
||||
id="control-select"
|
||||
value={selectedControlOption}
|
||||
onChange={onChangeSelect}
|
||||
input={<Input style={{margin: 0}} />}
|
||||
sx={{
|
||||
// Target the icon element directly within the Select
|
||||
'.MuiSelect-icon': {
|
||||
color: theme.outputTextColor, // Set your desired color
|
||||
},
|
||||
}}
|
||||
input={<Input style={{margin: 0, color: theme.outputTextColor}} />}
|
||||
>
|
||||
{InteractiveMessageTypes.map( (opt) => (
|
||||
<MenuItem value={opt} key={opt.name}>{opt.name}</MenuItem>
|
||||
{InteractiveMessageTypes.map( (opt,index) => (
|
||||
<MenuItem value={index} key={opt.name}>{opt.name}</MenuItem>
|
||||
) )}
|
||||
</Select>
|
||||
</FormControl>
|
||||
@@ -590,29 +612,34 @@ const InteractiveTaskingBar = ({
|
||||
<MythicTextField autoFocus={true} maxRows={5} multiline={true} onChange={onInputChange} onEnter={submitTask}
|
||||
value={inputText} variant={"standard"} placeholder={">_ type here..."} inline={true}
|
||||
debounceDelay={50}
|
||||
marginBottom={"0px"} InputProps={{style: { width: "100%"}}}/>
|
||||
<FormControl style={{width: "7rem"}} >
|
||||
marginBottom={"0px"} InputProps={{style: { width: "100%", color: theme.outputTextColor}}}/>
|
||||
<FormControl style={{width: "7rem", color: theme.outputTextColor}} >
|
||||
<Select
|
||||
labelId="linefeed-label"
|
||||
id="linefeed-control"
|
||||
value={selectedEnterOption}
|
||||
autoWidth
|
||||
sx={{
|
||||
// Target the icon element directly within the Select
|
||||
'.MuiSelect-icon': {
|
||||
color: theme.outputTextColor, // Set your desired color
|
||||
},
|
||||
}}
|
||||
onChange={onChangeSelectEnterOption}
|
||||
input={<Input />}
|
||||
input={<Input style={{color: theme.outputTextColor}} />}
|
||||
IconComponent={KeyboardReturnIcon}
|
||||
>
|
||||
{EnterOptions.map( (opt) => (
|
||||
<MenuItem value={opt} key={opt.name}>{opt.name}</MenuItem>
|
||||
{EnterOptions.map( (opt,index) => (
|
||||
<MenuItem value={index} key={opt.name}>{opt.name}</MenuItem>
|
||||
) )}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<MythicStyledTooltip title={useASNIColor ? "Disable ANSI Color" : "Enable ANSI Color"} >
|
||||
<IconButton onClick={toggleANSIColor} style={{paddingLeft: 0, paddingRight: 0}} disableRipple={true} disableFocusRipple={true}>
|
||||
<IconButton onClick={toggleANSIColor} style={{paddingLeft: 0, paddingRight: 0,}} disableRipple={true} disableFocusRipple={true}>
|
||||
<PaletteIcon color={useASNIColor ? "success" : "secondary"}
|
||||
style={{cursor: "pointer"}}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={showTaskStatus ? "Hide Task Status" : "Show Task Status"} >
|
||||
<IconButton onClick={toggleShowTaskStatus} style={{paddingLeft: 0, paddingRight: 0}} disableRipple={true} disableFocusRipple={true}>
|
||||
@@ -620,7 +647,6 @@ const InteractiveTaskingBar = ({
|
||||
style={{cursor: "pointer",}}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={wrapText ? "Unwrap Text" : "Wrap Text"} >
|
||||
<IconButton onClick={toggleWrapText} style={{paddingLeft: 0, paddingRight: 0}} disableRipple={true} disableFocusRipple={true}>
|
||||
@@ -628,17 +654,23 @@ const InteractiveTaskingBar = ({
|
||||
style={{cursor: "pointer"}}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={autoScroll ? "Stop Auto Scroll" : "Auto Scroll"} >
|
||||
<IconButton onClick={toggleAutoScroll} style={{paddingLeft: 0, paddingRight: 0}} disableRipple={true} disableFocusRipple={true}>
|
||||
<HeightIcon color={autoScroll ? "success" : "secondary"}
|
||||
style={{cursor: "pointer"}}
|
||||
/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const InteractivePaginationBar = ({totalCount, currentPage, onSubmitPageChange, pageSize, expand}) => {
|
||||
const InteractivePaginationBar = ({totalCount, currentPage, onSubmitPageChange, pageSize, selectAllOutput}) => {
|
||||
const onChangePage = (event, value) => {
|
||||
onSubmitPageChange(value);
|
||||
};
|
||||
const pageCount = Math.max(1, Math.ceil(totalCount / pageSize));
|
||||
if(pageCount < 2){
|
||||
if(pageCount < 2 || selectAllOutput){
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -41,18 +41,28 @@ import 'ace-builds/src-noconflict/mode-apache_conf';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import TabContext from '@mui/lab/TabContext';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import {previewFileQuery} from "../Search/FileMetaTable";
|
||||
import { useMutation, gql, useQuery } from '@apollo/client';
|
||||
import CodeIcon from '@mui/icons-material/Code';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import {b64DecodeUnicode} from "./ResponseDisplay";
|
||||
import {TableRowSizeCell} from "../../MythicComponents/MythicDialog";
|
||||
import {Table, TableHead, TableRow, TableBody} from '@mui/material';
|
||||
import {MythicDialog, TableRowSizeCell} from "../../MythicComponents/MythicDialog";
|
||||
import {Table, TableHead, TableRow, TableBody, TableCell, Paper} from '@mui/material';
|
||||
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
|
||||
import WarningOutlinedIcon from '@mui/icons-material/WarningOutlined';
|
||||
import {TagsDisplay, ViewEditTags} from "../../MythicComponents/MythicTag";
|
||||
import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery";
|
||||
import {ResponseDisplayScreenshotModal} from "./ResponseDisplayScreenshotModal";
|
||||
import initSQLJS from 'sql.js';
|
||||
import TabPanel from '@mui/lab/TabPanel';
|
||||
import TabList from '@mui/lab/TabList';
|
||||
import {Backdrop, CircularProgress} from '@mui/material';
|
||||
// Required to let webpack 4 know it needs to copy the wasm file to our assets
|
||||
// eslint-disable-next-line import/no-webpack-loader-syntax
|
||||
import sqlWasm from "!!file-loader?name=sql-wasm-[contenthash].wasm!sql.js/dist/sql-wasm.wasm";
|
||||
import {copyStringToClipboard} from "../../utilities/Clipboard";
|
||||
|
||||
export const modeOptions = ["csharp", "golang", "html", "json", "markdown", "ruby", "python", "java",
|
||||
"javascript", "yaml", "toml", "swift", "sql", "rust", "powershell", "pgsql", "perl", "php", "objectivec",
|
||||
@@ -112,7 +122,7 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
});
|
||||
React.useEffect( () => {
|
||||
fetchFileMetaData({variables: {agent_file_id: media?.agent_file_id}}).then( (data) => fetchedData(data)).catch((e) => console.log(e))
|
||||
}, [task.timestamp]);
|
||||
}, [task]);
|
||||
const handleChange = (event, newValue) => {
|
||||
setValue(newValue);
|
||||
}
|
||||
@@ -120,7 +130,6 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
<div style={{display: "flex", height: "100%", flexDirection: "column"}}>
|
||||
<DisplayFileMetaData fileMetaData={fileMetaData} />
|
||||
<AppBar color={'default'} position='static' className={"no-box-shadow"}>
|
||||
|
||||
<Tabs
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
@@ -138,7 +147,8 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
<Tab className={value === 0 ? "selectedCallback": ""} label={"Preview"}></Tab>
|
||||
<Tab className={value === 1 ? "selectedCallback": ""} label={"Text"}></Tab>
|
||||
<Tab className={value === 2 ? "selectedCallback": ""} label={"Hex"}></Tab>
|
||||
<MythicStyledTooltip title={"Download the file"} style={{display: "inline-flex"}}>
|
||||
<Tab className={value === 3 ? "selectedCallback": ""} label={"Database"}></Tab>
|
||||
<MythicStyledTooltip title={"Download the file"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<Button style={{}} size={"small"} href={"/direct/download/" + media.agent_file_id}
|
||||
download color={"success"}>
|
||||
<DownloadIcon />
|
||||
@@ -146,7 +156,7 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
</MythicStyledTooltip>
|
||||
</Tabs>
|
||||
</AppBar>
|
||||
<div hidden={value !== 0} style={{height: "100%"}} role='tabpanel' >
|
||||
<div hidden={value !== 0} style={{height: "100%", overflow: "auto"}} role='tabpanel' >
|
||||
{value === 0 &&
|
||||
<DisplayMedia agent_file_id={media?.agent_file_id || ""}
|
||||
task={task} filename={media?.filename || undefined}
|
||||
@@ -154,7 +164,7 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
expand={expand} />
|
||||
}
|
||||
</div>
|
||||
<div hidden={value !== 1} style={{height: "100%"}} role='tabpanel' >
|
||||
<div hidden={value !== 1} style={{height: "100%", overflow: "auto"}} role='tabpanel' >
|
||||
{value === 1 &&
|
||||
<DisplayText agent_file_id={media?.agent_file_id || ""}
|
||||
task={task} filename={media?.filename || undefined}
|
||||
@@ -162,7 +172,7 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
expand={expand} preview />
|
||||
}
|
||||
</div>
|
||||
<div hidden={value !== 2} style={{height: "100%"}} role='tabpanel' >
|
||||
<div hidden={value !== 2} style={{height: "100%", overflow: "auto"}} role='tabpanel' >
|
||||
{value === 2 &&
|
||||
<DisplayHex agent_file_id={media?.agent_file_id || ""}
|
||||
task={task} filename={media?.filename || undefined}
|
||||
@@ -171,6 +181,15 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
}
|
||||
|
||||
</div>
|
||||
<div hidden={value !== 3} style={{height: "100%", overflow: "auto"}} role='tabpanel' >
|
||||
{value === 3 &&
|
||||
<DisplayDatabase agent_file_id={media?.agent_file_id || ""}
|
||||
task={task} filename={media?.filename || undefined}
|
||||
fileMetaData={fileMetaData}
|
||||
expand={expand} />
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -216,13 +235,17 @@ export const textExtensionTypesToSyntax = {
|
||||
"python_history": "python",
|
||||
}
|
||||
const knownTextFiles = ["config", "credentials", "known_hosts", "config_default", "id_rsa", "dockerfile", "makefile"];
|
||||
const imgExtensionTypes = ["png", "jpg", "gif", "jpeg", "pdf"];
|
||||
const imgExtensionTypes = ["png", "jpg", "gif", "jpeg", "svg"];
|
||||
const renderExtensionTypes = ["pdf"];
|
||||
const mimeType = (path) => {
|
||||
if(!path){return undefined}
|
||||
let extension = path.split(".");
|
||||
if(extension.length > 1){
|
||||
extension = extension[extension.length - 1];
|
||||
if(imgExtensionTypes.includes(extension.toLowerCase())){
|
||||
return "image";
|
||||
}
|
||||
if(renderExtensionTypes.includes(extension.toLowerCase())){
|
||||
return "object";
|
||||
}
|
||||
if(textExtensionTypes.includes(extension.toLowerCase())){
|
||||
@@ -236,6 +259,7 @@ const mimeType = (path) => {
|
||||
}
|
||||
export const DisplayMedia = ({agent_file_id, filename, expand, task, fileMetaData}) => {
|
||||
const showMediaSetting = GetMythicSetting({setting_name: "showMedia", default_value: true});
|
||||
const [openScreenshot, setOpenScreenshot] = React.useState(false);
|
||||
const [showMedia, setShowMedia] = React.useState(showMediaSetting);
|
||||
const [fileData, setFileData] = React.useState({
|
||||
display: false,
|
||||
@@ -275,6 +299,11 @@ export const DisplayMedia = ({agent_file_id, filename, expand, task, fileMetaDat
|
||||
React.useLayoutEffect( () => {
|
||||
scrollContent()
|
||||
}, []);
|
||||
const clickedScreenshot = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setOpenScreenshot(true);
|
||||
}
|
||||
if(!showMedia){
|
||||
return (
|
||||
<>
|
||||
@@ -311,16 +340,38 @@ export const DisplayMedia = ({agent_file_id, filename, expand, task, fileMetaDat
|
||||
</object>
|
||||
)
|
||||
}
|
||||
if(fileData.display_type === "text"){
|
||||
if(fileData.display_type === "image"){
|
||||
return (
|
||||
<div style={{height: "100%", minHeight: "100px", width: "100%"}} >
|
||||
<DisplayText agent_file_id={agent_file_id} expand={expand} filename={filename} fileMetaData={fileMetaData} />
|
||||
<>
|
||||
<img width={"100%"} onClick={clickedScreenshot}
|
||||
src={"/direct/view/" + agent_file_id} style={{cursor: "zoom-in", width: "100%"}}>
|
||||
</img>
|
||||
{openScreenshot &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openScreenshot}
|
||||
onClose={() => {
|
||||
setOpenScreenshot(false);
|
||||
}}
|
||||
innerDialog={<ResponseDisplayScreenshotModal images={[agent_file_id]}
|
||||
onClose={() => {
|
||||
setOpenScreenshot(false);
|
||||
}}/>}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
if (fileData.display_type === "text") {
|
||||
return (
|
||||
<div style={{height: "100%", minHeight: "100px", width: "100%"}}>
|
||||
<DisplayText agent_file_id={agent_file_id} expand={expand} filename={filename}
|
||||
fileMetaData={fileMetaData}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const MaxRenderSize = 2000000;
|
||||
const MaxRenderSize = 2000000; // 2MB
|
||||
const DisplayFileMetaData = ({fileMetaData}) => {
|
||||
return (
|
||||
<Table style={{marginLeft: "0px", width: "100%", tableLayout: "fixed"}}>
|
||||
@@ -385,7 +436,10 @@ const DisplayText = ({agent_file_id, expand, filename, preview, fileMetaData}) =
|
||||
setLimitedPreviewWarning(true);
|
||||
}
|
||||
}else{
|
||||
snackActions.error(data.previewFile.error)
|
||||
console.log(data.previewFile.error);
|
||||
if(data.previewFile.error !== "EOF"){
|
||||
snackActions.error(data.previewFile.error);
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (data) => {
|
||||
@@ -445,7 +499,7 @@ const DisplayText = ({agent_file_id, expand, filename, preview, fileMetaData}) =
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [agent_file_id, filename]);
|
||||
}, [agent_file_id, filename, fileMetaData]);
|
||||
const onChangeMode = (event) => {
|
||||
setMode(event.target.value);
|
||||
}
|
||||
@@ -601,4 +655,255 @@ const DisplayHex = ({agent_file_id, expand, fileMetaData}) => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const MAX_ROWS = 1000;
|
||||
const DisplayDatabase = ({agent_file_id, expand, fileMetaData}) => {
|
||||
const theme = useTheme();
|
||||
const queryCountRef = React.useRef(1);
|
||||
const currentContentRef = React.useRef();
|
||||
const [selectedTab, setSelectedTab] = React.useState(0);
|
||||
const [sql, setSql] = React.useState(null);
|
||||
const [query, setQuery] = React.useState("select * from cookies;");
|
||||
const [results, setResults] = React.useState([]);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const onChangeText = (data) => {
|
||||
setQuery(data);
|
||||
}
|
||||
const removeQuery = (event, index) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if(selectedTab === index){
|
||||
if(index !== 0){
|
||||
setSelectedTab(index-1);
|
||||
}
|
||||
}
|
||||
setResults(results.toSpliced(index, 1));
|
||||
}
|
||||
const onSubmitQuery = (event) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
if(sql){
|
||||
try{
|
||||
const result = sql.exec(query);
|
||||
//console.log(result);
|
||||
if(result.length > 0){
|
||||
setResults([...results, {name: `Query: ${queryCountRef.current}`, query: query, results: result[0]}]);
|
||||
setSelectedTab(queryCountRef.current - 1);
|
||||
queryCountRef.current += 1;
|
||||
}
|
||||
}catch(e){
|
||||
console.log(e);
|
||||
snackActions.error(e.message);
|
||||
}
|
||||
} else {
|
||||
console.log(sql);
|
||||
snackActions.error("Failed to load as SQL Database");
|
||||
}
|
||||
}
|
||||
const onChangeTab = (_, v) => {
|
||||
const tabQuery = results[v]?.query;
|
||||
setQuery(tabQuery);
|
||||
setSelectedTab(v);
|
||||
}
|
||||
const onCopyToClipboard = (data) => {
|
||||
let result = copyStringToClipboard(data);
|
||||
if(result){
|
||||
snackActions.success("Copied text!");
|
||||
}else{
|
||||
snackActions.error("Failed to copy text");
|
||||
}
|
||||
}
|
||||
const onSaveOutputCSV = (result) => {
|
||||
const filteredData = result.results.values;
|
||||
let outputHeaders = "";
|
||||
let outputRow = "";
|
||||
outputHeaders = result.results.columns.join(",");
|
||||
for(let i = 0; i < filteredData.length; i++){
|
||||
outputRow += filteredData[i].join(",") + "\n"
|
||||
}
|
||||
onCopyToClipboard(outputHeaders + "\n" + outputRow);
|
||||
}
|
||||
const onSaveOutputPrettyPrint = (result) => {
|
||||
let tableData = result.results.values;
|
||||
if(tableData.length === 0){return}
|
||||
let outputHeaders = result.results.columns;
|
||||
let outputHeadersMaxLength = outputHeaders.reduce( (prev, cur) => {
|
||||
return [...prev, cur.length]
|
||||
}, []);
|
||||
tableData.map( cur => {
|
||||
for(let i = 0; i < outputHeaders.length; i++){
|
||||
if(outputHeadersMaxLength[i] < String(cur[i]).length){
|
||||
outputHeadersMaxLength[i] = String(cur[i]).length;
|
||||
}
|
||||
}
|
||||
});
|
||||
let allOutput = "";
|
||||
for(let i = 0; i < outputHeaders.length; i++){
|
||||
let remainingSpace = Math.max(0, outputHeadersMaxLength[i] - outputHeaders[i].length);
|
||||
allOutput += " " + outputHeaders[i].toUpperCase() + " ".repeat(remainingSpace + 2);
|
||||
}
|
||||
allOutput += "\n";
|
||||
for(let i = 0; i < outputHeaders.length; i++){
|
||||
let remainingSpace = Math.max(0, outputHeadersMaxLength[i] - outputHeaders[i].length);
|
||||
allOutput += " " + "-".repeat(outputHeaders[i].length) + " ".repeat(remainingSpace + 2);
|
||||
}
|
||||
allOutput += "\n";
|
||||
for(let i = 0; i < tableData.length; i++){
|
||||
for(let j = 0; j < outputHeaders.length; j++){
|
||||
let remainingSpace = Math.max(0, outputHeadersMaxLength[j] - String(tableData[i][j]).length);
|
||||
allOutput += "| " + String(tableData[i][j]) + " ".repeat(remainingSpace) + " ";
|
||||
}
|
||||
allOutput += " |\n"
|
||||
}
|
||||
onCopyToClipboard(allOutput);
|
||||
}
|
||||
React.useEffect( () => {
|
||||
async function initialize(){
|
||||
const newSQL = await initSQLJS({locateFile: () => sqlWasm});
|
||||
fetch('/direct/view/' + agent_file_id).then((response) => {
|
||||
if(response.status !== 200){
|
||||
setLoading(false);
|
||||
snackActions.warning("Failed to fetch contents from Mythic");
|
||||
return;
|
||||
}
|
||||
response.arrayBuffer().then( data => {
|
||||
const SQL = new newSQL.Database(new Uint8Array(data));
|
||||
if(SQL){
|
||||
setSql(SQL);
|
||||
}
|
||||
setLoading(false);
|
||||
}).catch(error => {
|
||||
setLoading(false);
|
||||
snackActions.warning("Error getting contents from server: " + error.toString());
|
||||
console.log("Error trying to get json response", error, response);
|
||||
});
|
||||
}).catch(error => {
|
||||
if(error.toString() === "TypeError: Failed to fetch"){
|
||||
snackActions.warning("Please refresh and accept the SSL connection error");
|
||||
} else {
|
||||
snackActions.warning("Error talking to server: " + error.toString());
|
||||
}
|
||||
setLoading(false);
|
||||
console.log("There was an error!", error);
|
||||
});
|
||||
}
|
||||
initialize();
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
// Execute query on Ctrl+Enter or Cmd+Enter when in the SQL editor
|
||||
if (e.shiftKey && e.key === 'Enter') {
|
||||
onSubmitQuery(e);
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [onSubmitQuery]);
|
||||
return (
|
||||
<div style={{display: "flex", height: "100%", flexDirection: "column", width: "100%", position: "relative"}}>
|
||||
<Backdrop open={loading} onClick={()=>{setLoading(false);}} style={{zIndex: 2, position: "absolute"}}>
|
||||
<CircularProgress color="inherit" disableShrink />
|
||||
</Backdrop>
|
||||
<div style={{display: "inline-flex", flexDirection: "row", alignItems: "center", borderBottom: "1px solid grey"}}>
|
||||
<AceEditor
|
||||
className={"roundedBottomCorners"}
|
||||
ref={currentContentRef}
|
||||
mode={'sql'}
|
||||
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
|
||||
fontSize={14}
|
||||
showGutter={ true}
|
||||
onChange={onChangeText}
|
||||
highlightActiveLine={false}
|
||||
showPrintMargin={false}
|
||||
value={query}
|
||||
height={"100%"}
|
||||
maxLines={3}
|
||||
width={"100%"}
|
||||
wrapEnabled={true}
|
||||
minLines={2}
|
||||
setOptions={{
|
||||
showLineNumbers: true,
|
||||
tabSize: 4,
|
||||
useWorker: false,
|
||||
showInvisibles: false,
|
||||
}}/>
|
||||
<Button color={"success"} variant={"contained"} onClick={onSubmitQuery} >Query</Button>
|
||||
</div>
|
||||
<TabContext style={{width: "100%", height: "100%", borderBottom: "1px solid grey", overflow: "auto"}} value={selectedTab}>
|
||||
<TabList onChange={onChangeTab} indicatorColor='secondary'
|
||||
textColor='primary'
|
||||
sx={{
|
||||
'& .Mui-selected': {
|
||||
color: "unset !important"
|
||||
}
|
||||
}}>
|
||||
{results.map((result, index) => (
|
||||
<Tab key={result.query + index} style={{padding: 0, paddingLeft: "10px", paddingRight: "10px"}} label={
|
||||
<div style={{display: "inline-block"}}>
|
||||
{result.name}
|
||||
<IconButton style={{padding: 0, marginLeft: "5px", marginRight: "5px", marginBottom: "5px"}}
|
||||
color={"error"}
|
||||
onClick={(e) => removeQuery(e, index)}>
|
||||
x
|
||||
</IconButton>
|
||||
</div>} value={index} className={selectedTab === index ? "selectedCallback" : ""} />
|
||||
))}
|
||||
</TabList>
|
||||
|
||||
{results.map((result, index) => (
|
||||
<div key={index} style={{display: selectedTab === index ? "flex" : "none",
|
||||
width: "100%", height: "100%", overflow: "auto",
|
||||
flexDirection: "column"}}>
|
||||
{result?.results?.values?.length >= MAX_ROWS &&
|
||||
<Paper key={"overflow" + index} style={{display: selectedTab === index ? "flex" : "none",
|
||||
justifyContent: "center", hidden: selectedTab !== index}}>
|
||||
<WarningOutlinedIcon color={"warning"}></WarningOutlinedIcon>
|
||||
{"Output is truncated to first " + MAX_ROWS + " rows"}
|
||||
<WarningOutlinedIcon color={"warning"}></WarningOutlinedIcon>
|
||||
</Paper>
|
||||
}
|
||||
<div key={"output" + index} style={{display: selectedTab === index ? "flex" : "none", alignItems: "center", width: "100%"}}>
|
||||
{"Save Output As: "}
|
||||
<Button variant={"outlined"} style={{marginLeft: "10px"}} onClick={() => onSaveOutputCSV(result)} >CSV</Button>
|
||||
<Button variant={"outlined"} style={{marginLeft: "10px"}} onClick={() => onSaveOutputPrettyPrint(result)} >Pretty Print</Button>
|
||||
</div>
|
||||
<TabPanel value={index} key={"tabpanel" + index}
|
||||
style={{padding: 0, height: "100%", width: "100%", overflow: "auto", position: "relative",
|
||||
display: selectedTab === index ? "flex" : "none",
|
||||
flexDirection: "column"}} >
|
||||
<DisplayDatabaseResult result={result?.results} />
|
||||
</TabPanel>
|
||||
</div>
|
||||
))}
|
||||
</TabContext>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const DisplayDatabaseResult = ({result}) => {
|
||||
const [columns, setColumns] = React.useState(result.columns);
|
||||
const [rows, setRows] = React.useState(result.values?.slice(0, MAX_ROWS));
|
||||
return (
|
||||
<Table stickyHeader style={{width: "100%", height: "100%", position: "absolute"}}>
|
||||
<TableHead >
|
||||
<TableRow>
|
||||
{columns.map((column, index) => (
|
||||
<TableCell key={"header" + index}>{column}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row, index1) => (
|
||||
<TableRow key={"row" + index1} hover>
|
||||
{columns.map((_, index2) => (
|
||||
<TableCell key={"row" + index1+ "cell" + index2}>
|
||||
{row[index2]}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
@@ -1,26 +1,14 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import AceEditor from 'react-ace';
|
||||
import 'ace-builds/src-noconflict/mode-json';
|
||||
import 'ace-builds/src-noconflict/theme-monokai';
|
||||
import 'ace-builds/src-noconflict/theme-xcode';
|
||||
import "ace-builds/src-noconflict/ext-searchbox";
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import {modeOptions} from "./ResponseDisplayMedia";
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import WrapTextIcon from '@mui/icons-material/WrapText';
|
||||
import 'ace-builds/src-noconflict/mode-csharp';
|
||||
import 'ace-builds/src-noconflict/mode-golang';
|
||||
import 'ace-builds/src-noconflict/mode-html';
|
||||
import 'ace-builds/src-noconflict/mode-markdown';
|
||||
import 'ace-builds/src-noconflict/mode-ruby';
|
||||
import 'ace-builds/src-noconflict/mode-python';
|
||||
import 'ace-builds/src-noconflict/mode-java';
|
||||
import 'ace-builds/src-noconflict/mode-javascript';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import { IconButton } from '@mui/material';
|
||||
import { IconButton, Select } from '@mui/material';
|
||||
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
|
||||
import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
|
||||
import { useReactiveVar } from '@apollo/client';
|
||||
@@ -28,6 +16,7 @@ import { meState } from '../../../cache';
|
||||
import CodeIcon from '@mui/icons-material/Code';
|
||||
import {GetOutputFormatAll} from "./ResponseDisplayInteractive";
|
||||
import PaletteIcon from '@mui/icons-material/Palette';
|
||||
import Input from '@mui/material/Input';
|
||||
|
||||
const MaxRenderSize = 2000000;
|
||||
export const ResponseDisplayPlaintext = (props) =>{
|
||||
@@ -108,14 +97,25 @@ export const ResponseDisplayPlaintext = (props) =>{
|
||||
return (
|
||||
<div style={{display: "flex", height: "100%", flexDirection: "column"}}>
|
||||
{showOptions &&
|
||||
<div style={{display: "inline-flex", flexDirection: "row", alignItems: "center"}}>
|
||||
<FormControl sx={{ display: "inline-block", marginLeft: "10px" }} size="small">
|
||||
<div style={{display: "inline-flex", flexDirection: "row", alignItems: "center",
|
||||
color: theme.outputTextColor,
|
||||
backgroundColor: theme.outputBackgroundColor + (theme.palette.mode === 'dark' ? "D0" : "20"),}}>
|
||||
<FormControl sx={{ display: "inline-block", marginLeft: "10px", color: theme.outputTextColor, }} size="small">
|
||||
<TextField
|
||||
label={"Syntax"}
|
||||
select
|
||||
margin={"dense"}
|
||||
size={"small"}
|
||||
style={{display: "inline-block", width: "100%"}}
|
||||
sx={{
|
||||
// Target the icon element directly within the Select
|
||||
'.MuiSelect-icon': {
|
||||
color: theme.outputTextColor, // Set your desired color
|
||||
},
|
||||
'.MuiInputBase-root': {
|
||||
color: theme.outputTextColor,
|
||||
}
|
||||
}}
|
||||
style={{display: "inline-block", width: "100%",}}
|
||||
value={mode}
|
||||
onChange={onChangeMode}
|
||||
>
|
||||
@@ -145,19 +145,33 @@ export const ResponseDisplayPlaintext = (props) =>{
|
||||
</MythicStyledTooltip>
|
||||
</div>
|
||||
}
|
||||
<div style={{height: "1px", width: "100%", display: "flex", zIndex: 1, backgroundColor: theme.palette.secondary.main}}>
|
||||
{showOptions &&
|
||||
<UnfoldLessIcon onClick={onChangeShowOptions} style={{cursor: "pointer", position: "relative", top: "-9px"}} />
|
||||
}
|
||||
{!showOptions &&
|
||||
<UnfoldMoreIcon onClick={onChangeShowOptions} style={{cursor: "pointer", position: "relative", top: "-9px"}} />
|
||||
}
|
||||
</div>
|
||||
{props.displayType !== 'console' &&
|
||||
<div style={{
|
||||
height: "1px",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
zIndex: 1,
|
||||
}}>
|
||||
{showOptions &&
|
||||
<UnfoldLessIcon onClick={onChangeShowOptions}
|
||||
style={{cursor: "pointer", position: "relative", top: "-9px"}}/>
|
||||
}
|
||||
{!showOptions &&
|
||||
<UnfoldMoreIcon onClick={onChangeShowOptions}
|
||||
style={{cursor: "pointer", position: "relative", top: "-9px"}}/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div style={{flexGrow: 1, height: "100%", paddingLeft: renderColors ? "15px" : "0px", paddingRight: renderColors ? "5px": "0px"}}>
|
||||
<div style={{
|
||||
flexGrow: 1,
|
||||
height: "100%",
|
||||
paddingLeft: renderColors ? "15px" : "0px",
|
||||
paddingRight: renderColors ? "5px" : "0px"
|
||||
}}>
|
||||
{renderColors &&
|
||||
<GetOutputFormatAll data={[
|
||||
{response: plaintextView, id: props?.task?.id || 0, timestamp: "1970-01-01"}]}
|
||||
{response: plaintextView, id: props?.task?.id || 0, timestamp: "1970-01-01"}]}
|
||||
myTask={false}
|
||||
taskID={props?.task?.id || 0}
|
||||
useASNIColor={true}
|
||||
@@ -170,10 +184,10 @@ export const ResponseDisplayPlaintext = (props) =>{
|
||||
<AceEditor
|
||||
className={"roundedBottomCorners"}
|
||||
ref={currentContentRef}
|
||||
mode={mode}
|
||||
mode={props.displayType !== 'console' ? mode : 'html'}
|
||||
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
|
||||
fontSize={14}
|
||||
showGutter={true}
|
||||
showGutter={ props.displayType !== 'console'}
|
||||
onChange={onChangeText}
|
||||
//onLoad={onLoad}
|
||||
highlightActiveLine={false}
|
||||
@@ -184,13 +198,13 @@ export const ResponseDisplayPlaintext = (props) =>{
|
||||
width={"100%"}
|
||||
//style={{backgroundColor: "transparent"}}
|
||||
//autoScrollEditorIntoView={true}
|
||||
wrapEnabled={wrapText}
|
||||
wrapEnabled={props.displayType !== 'console' ? wrapText : true}
|
||||
minLines={1}
|
||||
//maxLines={props.expand ? 50 : 20}
|
||||
setOptions={{
|
||||
showLineNumbers: true,
|
||||
showLineNumbers: props.displayType !== 'console',
|
||||
tabSize: 4,
|
||||
useWorker: false
|
||||
useWorker: false,
|
||||
showInvisibles: false,
|
||||
}}/>
|
||||
}
|
||||
|
||||
|
||||
@@ -3,17 +3,14 @@ import {Button} from '@mui/material';
|
||||
import {ResponseDisplayScreenshotModal} from './ResponseDisplayScreenshotModal';
|
||||
import { MythicDialog } from '../../MythicComponents/MythicDialog';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import {getSkewedNow} from "../../utilities/Time";
|
||||
|
||||
|
||||
export const ResponseDisplayScreenshot = (props) =>{
|
||||
const [openScreenshot, setOpenScreenshot] = React.useState(false);
|
||||
|
||||
const now = (getSkewedNow()).toUTCString();
|
||||
const clickOpenScreenshot = () => {
|
||||
setOpenScreenshot(true);
|
||||
}
|
||||
const scrollContent = (node, isAppearing) => {
|
||||
const scrollContent = (node, isAppearing) => {
|
||||
// only auto-scroll if you issued the task
|
||||
document.getElementById(`scrolltotaskbottom${props.task.id}`)?.scrollIntoView({
|
||||
//behavior: "smooth",
|
||||
@@ -21,21 +18,21 @@ export const ResponseDisplayScreenshot = (props) =>{
|
||||
inline: "nearest"
|
||||
})
|
||||
}
|
||||
React.useLayoutEffect( () => {
|
||||
React.useLayoutEffect( () => {
|
||||
scrollContent()
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
{openScreenshot &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openScreenshot}
|
||||
onClose={()=>{setOpenScreenshot(false);}}
|
||||
innerDialog={<ResponseDisplayScreenshotModal images={props.agent_file_id} onClose={()=>{setOpenScreenshot(false);}} />}
|
||||
/>
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openScreenshot}
|
||||
onClose={()=>{setOpenScreenshot(false);}}
|
||||
innerDialog={<ResponseDisplayScreenshotModal images={[props.agent_file_id]} onClose={()=>{setOpenScreenshot(false);}} />}
|
||||
/>
|
||||
}
|
||||
<pre style={{display: "inline-block"}}>
|
||||
{props?.plaintext || ""}
|
||||
</pre>
|
||||
<MythicStyledTooltip title={props?.hoverText || "View Screenshot"} >
|
||||
<MythicStyledTooltip title={props?.hoverText || "View Screenshot (s)"} >
|
||||
<Button color="primary" variant={props.variant ? props.variant : "contained"} onClick={clickOpenScreenshot} style={{marginBottom: "10px"}}>{props.name}</Button>
|
||||
</MythicStyledTooltip>
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, {useEffect, useRef} from 'react';
|
||||
import {Button} from '@mui/material';
|
||||
import { MythicViewJSONAsTableDialog, MythicDialog } from '../../MythicComponents/MythicDialog';
|
||||
import {MythicViewJSONAsTableDialog, MythicDialog, MythicModifyStringDialog} from '../../MythicComponents/MythicDialog';
|
||||
import { MythicDisplayTextDialog } from '../../MythicComponents/MythicDisplayTextDialog';
|
||||
import { ResponseDisplayTableDialogTable } from './ResponseDisplayTableDialogTable';
|
||||
import Paper from '@mui/material/Paper';
|
||||
@@ -20,6 +20,8 @@ import {faList, faTrashAlt, faSkullCrossbones, faCamera, faSyringe, faFolder, fa
|
||||
faFileImage, faCopy, faBoxOpen, faFileAlt, faCirclePlus, faCheck, faSquareXmark, faRotate } from '@fortawesome/free-solid-svg-icons';
|
||||
import {Dropdown, DropdownMenuItem} from "../../MythicComponents/MythicNestedMenus";
|
||||
import {GetComputedFontSize} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {TableFilterDialog} from "./TableFilterDialog";
|
||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||
|
||||
const onCopyToClipboard = (data) => {
|
||||
let result = copyStringToClipboard(data);
|
||||
@@ -87,7 +89,7 @@ export const getIconName = (iconName) => {
|
||||
case "camera":
|
||||
return faCamera;
|
||||
default:
|
||||
return faFileAlt;
|
||||
return iconName;
|
||||
}
|
||||
}
|
||||
export const getIconColor = (theme, color) => {
|
||||
@@ -358,9 +360,9 @@ const ResponseDisplayTableActionCellButton = ({cellData, callback_id}) => {
|
||||
<React.Fragment>
|
||||
<MythicStyledTooltip title={cellData?.button?.hoverText || "Submit Task"}>
|
||||
<Button size="small" onClick={() => setOpenTaskingButton(true)} disabled={cellData?.button?.disabled || false} color="warning"
|
||||
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.disabled ? "unset" : getIconColor(theme, cellData?.button?.startIconColor || "")}}/> : null}
|
||||
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.disabled ? "unset" : getIconColor(theme, cellData?.button?.startIconColor || "")}}/> : undefined}
|
||||
style={{...actionCellButtonStyle}}
|
||||
>{cellData?.button?.name ? cellData?.button?.name : cellData?.button?.startIcon ? null : "Submit Task"}</Button>
|
||||
>{cellData?.button?.name ? cellData?.button?.name : cellData?.button?.startIcon ? undefined : "Submit Task"}</Button>
|
||||
</MythicStyledTooltip>
|
||||
{openTaskingButton &&
|
||||
<TaskFromUIButton ui_feature={cellData?.button?.ui_feature || " "}
|
||||
@@ -406,11 +408,12 @@ const ResponseDisplayTableActionCellButton = ({cellData, callback_id}) => {
|
||||
table={taskingData?.value || {}} callback_id={callback_id} onClose={finishedViewingData} />}
|
||||
/>
|
||||
}
|
||||
<Button size="small" color="info" ref={dropdownAnchorRef}
|
||||
<Button size="small" ref={dropdownAnchorRef}
|
||||
onClick={() => setOpenDropdownButton(true)} disabled={cellData?.button?.disabled || false}
|
||||
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.disabled ? "unset" : getIconColor(theme, cellData?.button?.startIconColor || "")}}/> : null}
|
||||
style={{...actionCellButtonStyle}}
|
||||
>{cellData?.button?.name || " "}</Button>
|
||||
style={{...actionCellButtonStyle, color: theme.palette.text.primary}}
|
||||
>{cellData?.button?.name || " "} <ArrowDropDownIcon />
|
||||
</Button>
|
||||
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
|
||||
<Dropdown
|
||||
isOpen={dropdownAnchorRef.current}
|
||||
@@ -462,12 +465,41 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
const maxHeight = 375;
|
||||
const [dataHeight, setDataHeight] = React.useState(maxHeight);
|
||||
const [allData, setAllData] = React.useState([]);
|
||||
const [selectedColumn, setSelectedColumn] = React.useState({});
|
||||
const [filterOptions, setFilterOptions] = React.useState({});
|
||||
const [openContextMenu, setOpenContextMenu] = React.useState(false);
|
||||
const columns = React.useMemo(
|
||||
() =>
|
||||
table?.headers?.reduce( (prev, cur) => {
|
||||
if(filterOptions[cur.plaintext] && String(filterOptions[cur.plaintext]).length > 0){
|
||||
return [...prev, {...cur, filtered: true}];
|
||||
}else{
|
||||
return [...prev, {...cur}];
|
||||
}
|
||||
}, []) || []
|
||||
, [filterOptions, table.headers]
|
||||
);
|
||||
const [sortData, setSortData] = React.useState({sortKey: null, sortType: null, sortDirection: "ASC"})
|
||||
const filterRow = (row) => {
|
||||
for(const [key,value] of Object.entries(filterOptions)){
|
||||
if(!String(row[key].plaintext).toLowerCase().includes(String(value).toLowerCase())){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const sortedData = React.useMemo(() => {
|
||||
|
||||
const tmpData = [...allData].reduce( (prev, cur) => {
|
||||
if(filterRow(cur)){
|
||||
return [...prev];
|
||||
}else{
|
||||
return [...prev, cur];
|
||||
}
|
||||
}, []);
|
||||
if (sortData.sortKey === null || sortData.sortType === null) {
|
||||
return allData
|
||||
return tmpData
|
||||
}
|
||||
const tmpData = [...allData];
|
||||
if(sortData.sortType === "number" || sortData.sortType === "size"){
|
||||
tmpData.sort((a, b) => {
|
||||
if(a[sortData.sortKey]["plaintext"] === b[sortData.sortKey]["plaintext"]){
|
||||
@@ -526,7 +558,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
tmpData.reverse();
|
||||
}
|
||||
return tmpData;
|
||||
}, [allData, sortData]);
|
||||
}, [allData, sortData, filterOptions]);
|
||||
const onClickHeader = (e, columnIndex) => {
|
||||
const column = table.headers[columnIndex];
|
||||
|
||||
@@ -538,9 +570,9 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
}
|
||||
if (sortData.sortKey === column.plaintext) {
|
||||
if (sortData.sortDirection === 'ASC') {
|
||||
setSortData({sortKey: column.plaintext, sortType: column.type, sortDirection: "DESC"});
|
||||
setSortData({...sortData, sortDirection: "DESC"});
|
||||
} else {
|
||||
setSortData({sortKey: column.plaintext, sortType: column.type, sortDirection: "ASC"});
|
||||
setSortData({sortKey: null, sortType: null, sortDirection: "ASC"});
|
||||
}
|
||||
} else {
|
||||
setSortData({sortKey: column.plaintext, sortType: column.type, sortDirection: "ASC"});
|
||||
@@ -553,6 +585,10 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
});
|
||||
}, [sortedData, table?.headers, callback_id]
|
||||
);
|
||||
const onSubmitFilterOptions = (value) => {
|
||||
setFilterOptions({...filterOptions, [selectedColumn.key]: value });
|
||||
}
|
||||
|
||||
const filterOutButtonsFromRowData = (data) => {
|
||||
let rowData = {};
|
||||
for(const key of Object.keys(data)){
|
||||
@@ -562,7 +598,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
}
|
||||
return rowData;
|
||||
}
|
||||
const contextMenuOptions = [
|
||||
const rowContextMenuOptions = [
|
||||
{
|
||||
name: 'Copy Row as JSON', icon: null,
|
||||
click: ({event, columnIndex, rowIndex, data}) => {
|
||||
@@ -592,7 +628,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
},
|
||||
type: "item",
|
||||
},
|
||||
{
|
||||
{
|
||||
name: 'Copy Row as TSV', icon: null,
|
||||
click: ({event, columnIndex, rowIndex, data}) => {
|
||||
const filteredData = filterOutButtonsFromRowData(data);
|
||||
@@ -614,7 +650,148 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
},
|
||||
type: "item",
|
||||
},
|
||||
{
|
||||
name: 'Copy Cell', icon: null,
|
||||
click: ({event, columnIndex, rowIndex, data}) => {
|
||||
if(table.headers !== undefined && table.headers[columnIndex] !== undefined){
|
||||
let field = table.headers[columnIndex];
|
||||
if(field.plaintext !== undefined){ // column name
|
||||
if(data[field.plaintext].plaintext !== undefined){
|
||||
onCopyToClipboard(data[field.plaintext].plaintext);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, type: "item"
|
||||
},
|
||||
];
|
||||
const contextMenuOptions = [
|
||||
{
|
||||
name: 'Filter Column', type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if(table.headers[columnIndex].disableFilterMenu){
|
||||
snackActions.warning("Can't filter that column");
|
||||
return;
|
||||
}
|
||||
setSelectedColumn({
|
||||
"key": table.headers[columnIndex].plaintext,
|
||||
"name": table.headers[columnIndex].plaintext
|
||||
});
|
||||
setOpenContextMenu(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Copy Table as JSON", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
let tableData = sortedData.map(d => filterOutButtonsFromRowData(d))
|
||||
onCopyToClipboard(JSON.stringify(tableData, null, 2));
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Copy Table as CSV", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
let tableData = sortedData.map(d => filterOutButtonsFromRowData(d));
|
||||
if(tableData.length === 0){return}
|
||||
let outputHeaders = "";
|
||||
let allOutput = "";
|
||||
|
||||
for(let i = 0; i < tableData.length; i++){
|
||||
let outputRow = "";
|
||||
for(const key of Object.keys(tableData[i])){
|
||||
if(i === 0){
|
||||
if(outputHeaders === ""){
|
||||
outputHeaders += key;
|
||||
} else {
|
||||
outputHeaders += "," + key;
|
||||
}
|
||||
}
|
||||
if(outputRow === ""){
|
||||
outputRow += tableData[i][key];
|
||||
}else{
|
||||
outputRow += "," + tableData[i][key];
|
||||
}
|
||||
}
|
||||
allOutput += outputRow + "\n";
|
||||
}
|
||||
onCopyToClipboard(outputHeaders + "\n" + allOutput);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Copy Table as TSV", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
let tableData = sortedData.map(d => filterOutButtonsFromRowData(d));
|
||||
if(tableData.length === 0){return}
|
||||
let outputHeaders = "";
|
||||
let allOutput = "";
|
||||
|
||||
for(let i = 0; i < tableData.length; i++){
|
||||
let outputRow = "";
|
||||
for(const key of Object.keys(tableData[i])){
|
||||
if(i === 0){
|
||||
if(outputHeaders === ""){
|
||||
outputHeaders += key;
|
||||
} else {
|
||||
outputHeaders += "\t" + key;
|
||||
}
|
||||
}
|
||||
if(outputRow === ""){
|
||||
outputRow += tableData[i][key];
|
||||
}else{
|
||||
outputRow += "\t" + tableData[i][key];
|
||||
}
|
||||
}
|
||||
allOutput += outputRow + "\n";
|
||||
}
|
||||
onCopyToClipboard(outputHeaders + "\n" + allOutput);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Copy Table as PrettyPrint Text", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
let tableData = sortedData.map(d => filterOutButtonsFromRowData(d));
|
||||
if(tableData.length === 0){return}
|
||||
let outputHeaders = Object.keys(tableData[0]);
|
||||
let outputHeadersMaxLength = outputHeaders.reduce( (prev, cur) => {
|
||||
return {...prev, [cur]: cur.length}
|
||||
}, {});
|
||||
tableData.map( cur => {
|
||||
for(let i = 0; i < outputHeaders.length; i++){
|
||||
if(outputHeadersMaxLength[outputHeaders[i]] < String(cur[outputHeaders[i]]).length){
|
||||
outputHeadersMaxLength[outputHeaders[i]] = String(cur[outputHeaders[i]]).length;
|
||||
}
|
||||
}
|
||||
});
|
||||
let allOutput = "";
|
||||
for(let i = 0; i < outputHeaders.length; i++){
|
||||
let remainingSpace = Math.max(0, outputHeadersMaxLength[outputHeaders[i]] - outputHeaders[i].length);
|
||||
allOutput += " " + outputHeaders[i].toUpperCase() + " ".repeat(remainingSpace + 2);
|
||||
}
|
||||
allOutput += "\n";
|
||||
for(let i = 0; i < outputHeaders.length; i++){
|
||||
let remainingSpace = Math.max(0, outputHeadersMaxLength[outputHeaders[i]] - outputHeaders[i].length);
|
||||
allOutput += " " + "-".repeat(outputHeaders[i].length) + " ".repeat(remainingSpace + 2);
|
||||
}
|
||||
allOutput += "\n";
|
||||
for(let i = 0; i < tableData.length; i++){
|
||||
for(let j = 0; j < outputHeaders.length; j++){
|
||||
let remainingSpace = Math.max(0, outputHeadersMaxLength[outputHeaders[j]] - String(tableData[i][outputHeaders[j]]).length);
|
||||
allOutput += "| " + String(tableData[i][outputHeaders[j]]) + " ".repeat(remainingSpace) + " ";
|
||||
}
|
||||
allOutput += " |\n"
|
||||
}
|
||||
onCopyToClipboard(allOutput);
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
useEffect( () => {
|
||||
setAllData([...table.rows]);
|
||||
@@ -624,7 +801,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
const tableStyle = React.useMemo( () => {
|
||||
return expand ? {flexGrow: 1,
|
||||
minHeight: gridData.length > 0 ? Math.min(maxHeight, dataHeight) : headerHeight,
|
||||
width: "99%", position: "relative"} :
|
||||
width: "100%", position: "relative"} :
|
||||
{height: dataHeight, position: "relative"}
|
||||
}, [expand, dataHeight, gridData]);
|
||||
return (
|
||||
@@ -639,7 +816,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
|
||||
<div style={tableStyle}>
|
||||
<MythicResizableGrid
|
||||
columns={table.headers}
|
||||
columns={columns}
|
||||
sortIndicatorIndex={sortColumn}
|
||||
sortDirection={sortData.sortDirection}
|
||||
items={gridData}
|
||||
@@ -648,8 +825,24 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
onDoubleClickRow={doubleClickRow}
|
||||
rowHeight={rowHeight}
|
||||
onClickHeader={onClickHeader}
|
||||
rowContextMenuOptions={contextMenuOptions}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
rowContextMenuOptions={rowContextMenuOptions}
|
||||
/>
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={filterOptions[selectedColumn.key]}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import React from 'react';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import {a11yProps} from "../../MythicComponents/MythicTabPanel";
|
||||
import {ResponseDisplayBrowserScriptComponent} from "./ResponseDisplay";
|
||||
|
||||
function ResponseDisplayTabsLabel(props) {
|
||||
const { label, index, ...other } =
|
||||
props;
|
||||
return (
|
||||
<Tab
|
||||
label={
|
||||
<span>
|
||||
{label}
|
||||
</span>
|
||||
}
|
||||
sx={{
|
||||
'&.Mui-selected': {
|
||||
border: "2px solid grey",
|
||||
borderRadius: "4px",
|
||||
//backgroundColor: 'info.main', // Color when selected
|
||||
},
|
||||
}}
|
||||
wrapped={true}
|
||||
{...a11yProps(index)}
|
||||
{...other}
|
||||
/>
|
||||
);
|
||||
}
|
||||
function ResponseDisplayTabsPanel(props) {
|
||||
const { children, value, index, ...other } =
|
||||
props;
|
||||
const style =
|
||||
props.style === undefined
|
||||
? {
|
||||
display: value === index ? 'flex' : 'none',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
overflowY: "auto",
|
||||
}
|
||||
: props.style;
|
||||
return (
|
||||
<div
|
||||
role='tabpanel'
|
||||
hidden={value !== index}
|
||||
id={`scrollable-auto-tabpanel-${index}`}
|
||||
aria-labelledby={`scrollable-auto-tab-${index}`}
|
||||
style={style}
|
||||
{...other}>
|
||||
{<React.Fragment>{children}</React.Fragment>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export function ResponseDisplayTabs({ tabs, task, expand, displayType, output }) {
|
||||
const mountedRef = React.useRef(true);
|
||||
const [value, setValue] = React.useState(0);
|
||||
const handleChange = (event, newValue) => {
|
||||
setValue(newValue);
|
||||
};
|
||||
React.useEffect( () => {
|
||||
return() => {
|
||||
mountedRef.current = false;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div style={{width: "100%", maxWidth: "100%", display: 'flex', flexDirection: 'column', flexGrow: 1, height: expand ? "100%" : "400px", }}>
|
||||
<AppBar color='default' position={"static"} style={{maxHeight: "25%", overflowY: 'auto', maxWidth: "100%", flexWrap: "no-wrap"}} >
|
||||
<Tabs
|
||||
value={value}
|
||||
variant={"standard"}
|
||||
onChange={handleChange}
|
||||
indicatorColor='primary'
|
||||
textColor='primary'
|
||||
style={{ }}
|
||||
TabIndicatorProps={{style: {
|
||||
display: "none",
|
||||
}}}
|
||||
aria-label='scrollable tabs'>
|
||||
{tabs.map((tab, index) => (
|
||||
<ResponseDisplayTabsLabel
|
||||
key={'tablabel' + task.id + index}
|
||||
index={index}
|
||||
label={tab.title}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</AppBar>
|
||||
{tabs.map((tab, index) => (
|
||||
<ResponseDisplayTabsPanel
|
||||
key={'tabpanel' + task.id + index}
|
||||
value={value}
|
||||
index={index}>
|
||||
<ResponseDisplayBrowserScriptComponent
|
||||
task={task} expand={expand} displayType={displayType} output={output}
|
||||
browserScriptData={tab.content}
|
||||
/>
|
||||
</ResponseDisplayTabsPanel>
|
||||
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,8 +24,9 @@ export function TableFilterDialog({filterOptions, onSubmit, onClose, selectedCol
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Filter {selectedColumn.name} Entries</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
<DialogTitle id="form-dialog-title">Filter "{selectedColumn.name}" Entries</DialogTitle>
|
||||
<DialogContent style={{paddingBottom: 0}} dividers={true}>
|
||||
Show only rows that include the following case insensitive text:
|
||||
<MythicTextField autoFocus onChange={onChange} value={description} onEnter={onCommitSubmit}/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { styled } from '@mui/material/styles';
|
||||
import {IconButton, Link} from '@mui/material';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import {getSkewedNow, toLocalTime} from '../../utilities/Time';
|
||||
import {getSkewedNow, toLocalTime, toLocalTimeShort} from '../../utilities/Time';
|
||||
import Accordion from '@mui/material/Accordion';
|
||||
import AccordionSummary from '@mui/material/AccordionSummary';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
@@ -21,8 +21,16 @@ import CropRotateTwoToneIcon from '@mui/icons-material/CropRotateTwoTone';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import {operatorSettingDefaults} from "../../../cache";
|
||||
import {TaskFromUIButton} from "./TaskFromUIButton";
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {faSkullCrossbones} from '@fortawesome/free-solid-svg-icons';
|
||||
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
||||
// Icons for console-view display
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import PersonOutlineIcon from '@mui/icons-material/PersonOutline';
|
||||
import ComputerIcon from '@mui/icons-material/Computer';
|
||||
import PublicIcon from '@mui/icons-material/Public';
|
||||
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
|
||||
import NumbersIcon from '@mui/icons-material/Numbers';
|
||||
|
||||
|
||||
const PREFIX = 'TaskDisplay';
|
||||
@@ -55,7 +63,7 @@ export const StyledPaper = styled(Paper)((
|
||||
marginTop: "3px",
|
||||
marginRight: "0px",
|
||||
height: "auto",
|
||||
width: "99%",
|
||||
width: "100%",
|
||||
boxShadow: "unset",
|
||||
backgroundColor: theme.palette.background.default + "CC",
|
||||
},
|
||||
@@ -69,10 +77,11 @@ export const StyledPaper = styled(Paper)((
|
||||
WebkitBoxOrient: "vertical",
|
||||
cursor: "default",
|
||||
wordBreak: "break-all",
|
||||
//color: theme.taskPromptTextColor,
|
||||
},
|
||||
[`& .${classes.secondaryHeading}`]: {
|
||||
fontSize: theme.typography.pxToRem(15),
|
||||
//color: theme.palette.text.secondary,
|
||||
//color: theme.taskPromptTextColor,
|
||||
overflow: "auto",
|
||||
display: "block",
|
||||
textOverflow: "ellipsis",
|
||||
@@ -81,7 +90,7 @@ export const StyledPaper = styled(Paper)((
|
||||
},
|
||||
[`& .${classes.taskAndTimeDisplay}`]: {
|
||||
fontSize: theme.typography.pxToRem(12),
|
||||
color: theme.palette.text.secondary,
|
||||
color: theme.taskPromptTextColor,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
maxWidth: "100%",
|
||||
@@ -92,7 +101,7 @@ export const StyledPaper = styled(Paper)((
|
||||
},
|
||||
[`& .${classes.secondaryHeadingExpanded}`]: {
|
||||
fontSize: theme.typography.pxToRem(15),
|
||||
//color: theme.palette.text.secondary,
|
||||
//color: theme.taskPromptTextColor,
|
||||
display: "block",
|
||||
overflow: "auto",
|
||||
maxWidth: "100%",
|
||||
@@ -131,19 +140,21 @@ export const StyledAccordionSummary = styled(AccordionSummary)((
|
||||
) => ({
|
||||
[`&.${accordionClasses.root}`]: {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
paddingLeft: 0,
|
||||
//paddingRight: 0,
|
||||
height: "auto",
|
||||
width: "100%",
|
||||
whiteSpace: "break-all",
|
||||
wordBreak: "break-all",
|
||||
userSelect: "text",
|
||||
boxShadow: "unset",
|
||||
backgroundColor: "unset",
|
||||
justifyContent: "flex-start",
|
||||
},
|
||||
[`& .${accordionClasses.content}`]: {
|
||||
margin: 0,
|
||||
height: "100%",
|
||||
padding: 0,
|
||||
width: "inherit",
|
||||
},
|
||||
[`& .${accordionClasses.expandIcon}`]: {
|
||||
margin: 0,
|
||||
@@ -216,14 +227,16 @@ const ColoredTaskDisplay = ({task, theme, children, expanded}) => {
|
||||
setThemeColor(theme.palette.warning.main);
|
||||
}else if(task.status.toLowerCase().includes("processing")){
|
||||
setThemeColor(theme.palette.warning.main);
|
||||
}else if(task.status === "completed" || (task.status === "success" && task.completed)){
|
||||
}else if(task.status === "completed" || task.completed){
|
||||
setThemeColor(theme.palette.success.main);
|
||||
}else{
|
||||
setThemeColor(theme.palette.info.main);
|
||||
}
|
||||
}, [task.status, task.completed])
|
||||
return(
|
||||
<span style={{display: "flex", margin: 0, borderWidth: 0, padding: 0, minHeight: "48px", alignItems: "center", height: "100%", borderLeft: "6px solid " + themeColor, paddingLeft: "5px", width: "100%", borderTopLeftRadius: "4px", borderBottomLeftRadius: expanded ? 0 : "4px"}}>
|
||||
<span style={{display: "flex", margin: 0, borderWidth: 0, padding: 0, minHeight: "48px", alignItems: "center",
|
||||
height: "100%", borderLeft: "6px solid " + themeColor, paddingLeft: "5px", width: "100%", maxWidth: "100%",
|
||||
borderTopLeftRadius: "4px", borderBottomLeftRadius: expanded ? 0 : "4px"}}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
@@ -232,7 +245,7 @@ const GetOperatorDisplay = ({initialHideUsernameValue, task}) => {
|
||||
if(initialHideUsernameValue){
|
||||
return '';
|
||||
}
|
||||
return "/ " + task.operator.username;
|
||||
return " / " + task.operator.username;
|
||||
}
|
||||
export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayChildren, toggleDisplayChildren, expanded }) => {
|
||||
const [displayComment, setDisplayComment] = React.useState(false);
|
||||
@@ -243,6 +256,7 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh
|
||||
const initialShowHostnameValue = GetMythicSetting({setting_name: "showHostname", default_value: operatorSettingDefaults.showHostname});
|
||||
const initialShowCallbackGroupsValue = GetMythicSetting({setting_name: "showCallbackGroups", default_value: operatorSettingDefaults.showCallbackGroups});
|
||||
const initialTaskTimestampDisplayField = GetMythicSetting({setting_name: "taskTimestampDisplayField", default_value: operatorSettingDefaults.taskTimestampDisplayField});
|
||||
const initialShowOPSECBypassUsername = GetMythicSetting({setting_name: "showOPSECBypassUsername", default_value: operatorSettingDefaults.showOPSECBypassUsername});
|
||||
const displayTimestamp = task[initialTaskTimestampDisplayField] ? task[initialTaskTimestampDisplayField] : task.timestamp;
|
||||
const [openKillTaskButton, setOpenKillTaskButton] = React.useState({open: false});
|
||||
const toggleDisplayComment = (evt) => {
|
||||
@@ -257,7 +271,6 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh
|
||||
if(onClick){
|
||||
onClick(e);
|
||||
}
|
||||
//preventPropagation(e);
|
||||
}
|
||||
const onClickKillIcon = (e, open) => {
|
||||
if(e){
|
||||
@@ -266,7 +279,7 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh
|
||||
setOpenKillTaskButton({open: open});
|
||||
}
|
||||
return (
|
||||
<ColoredTaskDisplay task={task} theme={theme} expanded={expanded} >
|
||||
<ColoredTaskDisplay task={task} theme={theme} expanded={expanded} >
|
||||
<div id={taskDivID} style={{width: "100%"}}>
|
||||
{displayComment && (
|
||||
<React.Fragment>
|
||||
@@ -274,84 +287,92 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh
|
||||
<Typography className={classes.heading} onClick={preventPropagation}>{task.comment}</Typography>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<div style={{lineHeight: 0}}>
|
||||
<div style={{lineHeight: 0}} onClick={onLocalClick}>
|
||||
<Typography className={classes.taskAndTimeDisplay} onClick={preventPropagation}>
|
||||
[{toLocalTime(displayTimestamp, me?.user?.view_utc_time || false)}]
|
||||
{" / "}
|
||||
<span style={{}}>
|
||||
{task.has_intercepted_response &&
|
||||
<>
|
||||
<MythicStyledTooltip
|
||||
title={"This task has responses that have been intercepted and changed due to a workflow container"}>
|
||||
<IconButton style={{padding: 0}} color={"secondary"}>
|
||||
<CropRotateTwoToneIcon fontSize={"small"}/>
|
||||
</IconButton>
|
||||
|
||||
</MythicStyledTooltip>
|
||||
{"/ "}
|
||||
</>
|
||||
}
|
||||
{task?.eventstepinstance !== null &&
|
||||
<>
|
||||
<MythicStyledTooltip title={"Task created via Eventing, click to view entire event flow in separate page"} >
|
||||
<IconButton component={Link} href={'/new/eventing?eventgroup=' +
|
||||
task?.eventstepinstance?.eventgroupinstance?.eventgroup?.id +
|
||||
"&eventgroupinstance=" + task?.eventstepinstance?.eventgroupinstance?.id
|
||||
} target={"_blank"} style={{padding: 0}}
|
||||
color="inherit" disableFocusRipple={true}
|
||||
{task.has_intercepted_response &&
|
||||
<span style={{display:"inline-flex", whiteSpace: "pre-wrap"}}>
|
||||
<MythicStyledTooltip
|
||||
title={"This task has responses that have been intercepted and changed due to a workflow container"}>
|
||||
<IconButton color={"inherit"} style={{padding: 0}} disableFocusRipple={true}
|
||||
disableRipple={true}>
|
||||
<PlayCircleFilledTwoToneIcon />
|
||||
<CropRotateTwoToneIcon style={{height: "15px", cursor: "default"}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
|
||||
{"/ "}
|
||||
</>
|
||||
}
|
||||
<MythicStyledTooltip title={"View Task in separate page"} >
|
||||
<Link style={{wordBreak: "break-all"}} color={"textPrimary"} underline={"always"} target={"_blank"}
|
||||
href={"/new/task/" + task.display_id}>T-{task.display_id}</Link>
|
||||
</MythicStyledTooltip>
|
||||
{" / "}
|
||||
</span>
|
||||
}
|
||||
{task?.eventstepinstance !== null &&
|
||||
<span style={{display:"inline-flex", whiteSpace: "pre-wrap"}}>
|
||||
<MythicStyledTooltip title={"Task created via Eventing, click to view entire event flow in separate page"} >
|
||||
<IconButton component={Link} href={'/new/eventing?eventgroup=' +
|
||||
task?.eventstepinstance?.eventgroupinstance?.eventgroup?.id +
|
||||
"&eventgroupinstance=" + task?.eventstepinstance?.eventgroupinstance?.id
|
||||
} target={"_blank"} style={{padding: 0, }}
|
||||
color="inherit" disableFocusRipple={true}
|
||||
disableRipple={true}>
|
||||
<PlayCircleFilledTwoToneIcon fontSize={"small"} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
|
||||
{" / "}
|
||||
</span>
|
||||
}
|
||||
<span style={{display:"inline-flex", whiteSpace: "pre-wrap"}}>
|
||||
<MythicStyledTooltip title={"View Task in separate page"} >
|
||||
<Link style={{wordBreak: "break-all", color: theme.taskPromptTextColor,}} underline={"always"} target={"_blank"}
|
||||
href={"/new/task/" + task.display_id}>T-{task.display_id}</Link>
|
||||
</MythicStyledTooltip>
|
||||
{!task.completed && task.status_timestamp_processing &&
|
||||
<>
|
||||
<MythicStyledTooltip title={"Task the agent to kill this task"} >
|
||||
<IconButton style={{padding: 0}} onClick={(e) => onClickKillIcon(e, true)}
|
||||
color="inherit" disableFocusRipple={true}
|
||||
disableRipple={true}>
|
||||
<FontAwesomeIcon size={"sm"} icon={faSkullCrossbones}
|
||||
style={{cursor: "pointer", height: "15px", marginLeft: "5px"}} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</>
|
||||
|
||||
<MythicStyledTooltip title={"Task the agent to kill this task"} >
|
||||
<FontAwesomeIcon size={"sm"} icon={faSkullCrossbones} onClick={(e) => onClickKillIcon(e, true)}
|
||||
style={{cursor: "pointer", height: "12px", marginLeft: "5px"}} />
|
||||
</MythicStyledTooltip>
|
||||
|
||||
}
|
||||
</span>
|
||||
{" "}
|
||||
<GetOperatorDisplay initialHideUsernameValue={initialHideUsernameValue} task={task}/>
|
||||
{" / "}
|
||||
<MythicStyledTooltip title={"View Callback in separate page"}>
|
||||
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
|
||||
<Link style={{wordBreak: "break-all", color: theme.taskPromptTextColor}} underline="always" target="_blank"
|
||||
href={"/new/callbacks/" + task.callback.display_id}>C-{task.callback.display_id}</Link>
|
||||
</MythicStyledTooltip>
|
||||
|
||||
{initialShowHostnameValue ? ` / ${task.callback.host} ` : ''}
|
||||
{initialShowIPValue ? `/ ${ipValue} ` : ''}
|
||||
{initialShowCallbackGroupsValue ? `/ ${task.callback.mythictree_groups.join(', ')} ` : ''}
|
||||
{" / "}
|
||||
{task?.command?.payloadtype?.name}
|
||||
{" / "}
|
||||
{initialShowOPSECBypassUsername && <>
|
||||
{(task?.opsec_pre_bypass_user?.username || task?.opsec_post_bypass_user?.username) && <span
|
||||
style={{display:"inline-flex", whiteSpace: "pre-wrap"}}>
|
||||
{" /"}
|
||||
<MythicStyledTooltip title={"The specified usernames approved OPSEC bypasses for this task"}
|
||||
tooltipStyle={{padding: 0}}>
|
||||
<LockOpenIcon color={"inherit"} style={{height: "15px"}}/>
|
||||
</MythicStyledTooltip>
|
||||
{task?.opsec_pre_bypass_user?.username}
|
||||
{task?.opsec_pre_bypass_user?.username && task?.opsec_post_bypass_user?.username && ", "}
|
||||
{task?.opsec_post_bypass_user?.username}
|
||||
</span>}
|
||||
</>}
|
||||
{" "}
|
||||
<TaskStatusDisplay task={task} theme={theme}/>
|
||||
{task.comment.length > 0 ? (
|
||||
{task.comment.length > 0 &&
|
||||
<span className={classes.column}>
|
||||
{" / "}
|
||||
<IconButton size="small" style={{padding: "0"}}
|
||||
onClick={toggleDisplayComment}><ChatOutlinedIcon fontSize={"small"}/></IconButton>
|
||||
</span>
|
||||
) : null}
|
||||
}
|
||||
{task.comment}
|
||||
</Typography>
|
||||
</Typography>
|
||||
<TaskTagDisplay task={task}/>
|
||||
</div>
|
||||
<div>
|
||||
<div onClick={onLocalClick}>
|
||||
|
||||
<div className={classes.column} onClick={onLocalClick}>
|
||||
<div className={classes.column} >
|
||||
<Badge badgeContent={alertBadges} color="warning" anchorOrigin={{vertical: 'top', horizontal: 'left'}}>
|
||||
{task.tasks.length > 0 && !displayChildren &&
|
||||
<ExpandMoreIcon onClick={toggleDisplayChildren} />
|
||||
@@ -363,7 +384,7 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh
|
||||
enterDelay={2000}
|
||||
placement={"top"}
|
||||
title={(task?.command?.cmd || task.command_name) + " " + task.display_params} >
|
||||
<Typography className={classes.heading} onClick={onLocalClick} >
|
||||
<Typography className={classes.heading} style={{color: theme.taskPromptCommandTextColor}} onClick={onLocalClick} >
|
||||
{(task?.command?.cmd || task.command_name) + " " + task.display_params}
|
||||
</Typography>
|
||||
</MythicStyledTooltip>
|
||||
@@ -419,7 +440,7 @@ const TaskRow = ({task, filterOptions, me, newlyIssuedTasks, indentLevel, collap
|
||||
"parameterString": onlyParameters,
|
||||
"hideErrors": hideErrors
|
||||
}); */
|
||||
if(hideBrowserTasking && task.tasking_location.includes("browser")){
|
||||
if(hideBrowserTasking && task.tasking_location.includes("_browser")){
|
||||
setShouldDisplay(false);
|
||||
return;
|
||||
}
|
||||
@@ -491,6 +512,10 @@ const TaskRow = ({task, filterOptions, me, newlyIssuedTasks, indentLevel, collap
|
||||
if(window.getSelection().toString() !== ""){
|
||||
return;
|
||||
}
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
setDropdownOpen(!dropdownOpen);
|
||||
}, [dropdownOpen]);
|
||||
const toggleDisplayChildren = React.useCallback( (event, expanded) => {
|
||||
@@ -498,6 +523,10 @@ const TaskRow = ({task, filterOptions, me, newlyIssuedTasks, indentLevel, collap
|
||||
return;
|
||||
}
|
||||
setDisplayChildren(!displayChildren);
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
}, [displayChildren]);
|
||||
useEffect( () => {
|
||||
if(collapseAllRequest > 0){
|
||||
@@ -653,11 +682,14 @@ const TaskRowFlat = ({task, filterOptions, me, onSelectTask, showOnSelectTask, s
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}, [displayChildren]);
|
||||
const onLocalSelectTask = React.useCallback( () => {
|
||||
onSelectTask(task);
|
||||
}, [task]);
|
||||
return (
|
||||
shouldDisplay && (
|
||||
<div style={{marginLeft: (indentLevel * 10) + "px"}}>
|
||||
<TaskLabelFlat me={me} task={task}
|
||||
onSelectTask={() => {onSelectTask(task)}}
|
||||
onSelectTask={onLocalSelectTask}
|
||||
showOnSelectTask={showOnSelectTask}
|
||||
toggleDisplayChildren={toggleDisplayChildren} displayChildren={displayChildren}
|
||||
/>
|
||||
@@ -674,130 +706,6 @@ const TaskRowFlat = ({task, filterOptions, me, onSelectTask, showOnSelectTask, s
|
||||
)
|
||||
)
|
||||
}
|
||||
const TaskRowConsole = ({task, filterOptions, me, newlyIssuedTasks, indentLevel}) => {
|
||||
const [dropdownOpen, setDropdownOpen] = React.useState(false);
|
||||
const [taskingData, setTaskingData] = React.useState([]);
|
||||
const [shouldDisplay, setShouldDisplay] = React.useState(true);
|
||||
const hideBrowserTasking = GetMythicSetting({setting_name: "hideBrowserTasking", default_value: operatorSettingDefaults.hideBrowserTasking});
|
||||
|
||||
useSubscription(getSubTaskingQuery, {
|
||||
variables: {task_id: task.id},
|
||||
onData: ({data}) => {
|
||||
//console.log(subscriptionData);
|
||||
// need to merge in the tasking data
|
||||
const newTaskingData = data.data.task_stream.reduce( (prev, cur) => {
|
||||
for(let i = 0; i < prev.length; i++){
|
||||
if(prev[i].id === cur.id){
|
||||
prev[i] = {...cur}
|
||||
return prev;
|
||||
}
|
||||
}
|
||||
return [...prev, cur];
|
||||
}, [...taskingData]);
|
||||
newTaskingData.sort( (a,b) => a.id < b.id ? -1 : 1);
|
||||
setTaskingData(newTaskingData);
|
||||
}
|
||||
});
|
||||
useEffect( () => {
|
||||
/*props.onSubmit({
|
||||
"operatorsList": onlyOperators,
|
||||
"commentsFlag": onlyHasComments,
|
||||
"commandsList": onlyCommands,
|
||||
"everythingButList": everythingBut,
|
||||
"parameterString": onlyParameters,
|
||||
"hideErrors": hideErrors
|
||||
}); */
|
||||
if(hideBrowserTasking && task.tasking_location.includes("browser")){
|
||||
setShouldDisplay(false);
|
||||
return;
|
||||
}
|
||||
if(task.display_params.includes("help") && task.operator.username !== me.user.username){
|
||||
setShouldDisplay(false);
|
||||
return;
|
||||
}
|
||||
if(filterOptions === undefined){
|
||||
if(!shouldDisplay){
|
||||
setShouldDisplay(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(filterOptions["operatorsList"].length > 0){
|
||||
if(!filterOptions["operatorsList"].includes(task.operator.username)){
|
||||
if(shouldDisplay){
|
||||
setShouldDisplay(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(filterOptions["commentsFlag"]){
|
||||
if(task.comment === ""){
|
||||
if(shouldDisplay){
|
||||
setShouldDisplay(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(filterOptions["commandsList"].length > 0){
|
||||
// only show these commands
|
||||
if(!filterOptions["commandsList"].includes(task.command_name)){
|
||||
if(shouldDisplay){
|
||||
setShouldDisplay(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(filterOptions["everythingButList"].length > 0){
|
||||
if(filterOptions["everythingButList"].includes(task.command_name)){
|
||||
if(shouldDisplay){
|
||||
setShouldDisplay(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(filterOptions["parameterString"] !== ""){
|
||||
let regex = new RegExp(filterOptions["parameterString"]);
|
||||
if(!regex.test(task.display_params)){
|
||||
if(shouldDisplay){
|
||||
setShouldDisplay(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(filterOptions["hideErrors"]){
|
||||
if(task.status.includes("error")){
|
||||
if(shouldDisplay){
|
||||
setShouldDisplay(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(!shouldDisplay){
|
||||
setShouldDisplay(true);
|
||||
}
|
||||
}, [filterOptions, task.comment, task.command, task.display_params, task.operator.username]);
|
||||
const toggleTaskDropdown = React.useCallback( (event, expanded) => {
|
||||
if(window.getSelection().toString() !== ""){
|
||||
return;
|
||||
}
|
||||
setDropdownOpen(!dropdownOpen);
|
||||
}, [dropdownOpen]);
|
||||
|
||||
return (
|
||||
shouldDisplay ? (
|
||||
<div style={{marginLeft: (indentLevel * 10) + "px"}}>
|
||||
<TaskLabelConsole me={me} task={task} newlyIssuedTasks={newlyIssuedTasks} />
|
||||
{
|
||||
taskingData.map( (tsk) => (
|
||||
<TaskRowConsole key={"taskrow: " + tsk.id} me={me} task={tsk}
|
||||
filterOptions={filterOptions} indentLevel={indentLevel+1}/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
) : null
|
||||
)
|
||||
}
|
||||
|
||||
const TaskLabel = ({task, dropdownOpen, toggleTaskDropdown, me, newlyIssuedTasks, displayChildren, toggleDisplayChildren}) => {
|
||||
const [fromNow, setFromNow] = React.useState(getSkewedNow());
|
||||
const theme = useTheme();
|
||||
@@ -899,7 +807,7 @@ export const TaskLabelFlat = ({task, me, showOnSelectTask, onSelectTask, graphVi
|
||||
}
|
||||
|
||||
return(
|
||||
<StyledPaper className={task.selected ? classes.root + " selectedTask no-box-shadow" : classes.root}
|
||||
<StyledPaper className={task.selected && showOnSelectTask ? classes.root + " selectedTask no-box-shadow" : classes.root}
|
||||
elevation={5} style={{marginRight: 0, cursor: "pointer"}} id={`taskHeader-${task.id}`}
|
||||
onClick={onClickEntry}
|
||||
>
|
||||
@@ -909,6 +817,261 @@ export const TaskLabelFlat = ({task, me, showOnSelectTask, onSelectTask, graphVi
|
||||
</StyledPaper>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const ColoredTaskDisplayConsole = ({task, theme, children, expanded}) => {
|
||||
return(
|
||||
<span style={{display: "flex", margin: 0, borderWidth: 0, padding: 0, minHeight: "30px", alignItems: "center",
|
||||
height: "100%", paddingLeft: "5px", width: "100%",
|
||||
borderTopLeftRadius: "4px", borderBottomLeftRadius: expanded ? 0 : "4px"}}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
const TaskRowConsole = ({task, filterOptions, me, newlyIssuedTasks, indentLevel}) => {
|
||||
const [taskingData, setTaskingData] = React.useState([]);
|
||||
const [shouldDisplay, setShouldDisplay] = React.useState(true);
|
||||
const hideBrowserTasking = GetMythicSetting({setting_name: "hideBrowserTasking", default_value: operatorSettingDefaults.hideBrowserTasking});
|
||||
|
||||
useSubscription(getSubTaskingQuery, {
|
||||
variables: {task_id: task.id},
|
||||
onData: ({data}) => {
|
||||
//console.log(subscriptionData);
|
||||
// need to merge in the tasking data
|
||||
const newTaskingData = data.data.task_stream.reduce( (prev, cur) => {
|
||||
for(let i = 0; i < prev.length; i++){
|
||||
if(prev[i].id === cur.id){
|
||||
prev[i] = {...cur}
|
||||
return prev;
|
||||
}
|
||||
}
|
||||
return [...prev, cur];
|
||||
}, [...taskingData]);
|
||||
newTaskingData.sort( (a,b) => a.id < b.id ? -1 : 1);
|
||||
setTaskingData(newTaskingData);
|
||||
}
|
||||
});
|
||||
useEffect( () => {
|
||||
/*props.onSubmit({
|
||||
"operatorsList": onlyOperators,
|
||||
"commentsFlag": onlyHasComments,
|
||||
"commandsList": onlyCommands,
|
||||
"everythingButList": everythingBut,
|
||||
"parameterString": onlyParameters,
|
||||
"hideErrors": hideErrors
|
||||
}); */
|
||||
if(hideBrowserTasking && task.tasking_location.includes("browser")){
|
||||
setShouldDisplay(false);
|
||||
return;
|
||||
}
|
||||
if(task.display_params.includes("help") && task.operator.username !== me.user.username){
|
||||
setShouldDisplay(false);
|
||||
return;
|
||||
}
|
||||
if(filterOptions === undefined){
|
||||
if(!shouldDisplay){
|
||||
setShouldDisplay(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(filterOptions["operatorsList"].length > 0){
|
||||
if(!filterOptions["operatorsList"].includes(task.operator.username)){
|
||||
if(shouldDisplay){
|
||||
setShouldDisplay(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(filterOptions["commentsFlag"]){
|
||||
if(task.comment === ""){
|
||||
if(shouldDisplay){
|
||||
setShouldDisplay(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(filterOptions["commandsList"].length > 0){
|
||||
// only show these commands
|
||||
if(!filterOptions["commandsList"].includes(task.command_name)){
|
||||
if(shouldDisplay){
|
||||
setShouldDisplay(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(filterOptions["everythingButList"].length > 0){
|
||||
if(filterOptions["everythingButList"].includes(task.command_name)){
|
||||
if(shouldDisplay){
|
||||
setShouldDisplay(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(filterOptions["parameterString"] !== ""){
|
||||
let regex = new RegExp(filterOptions["parameterString"]);
|
||||
if(!regex.test(task.display_params)){
|
||||
if(shouldDisplay){
|
||||
setShouldDisplay(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(filterOptions["hideErrors"]){
|
||||
if(task.status.includes("error")){
|
||||
if(shouldDisplay){
|
||||
setShouldDisplay(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(!shouldDisplay){
|
||||
setShouldDisplay(true);
|
||||
}
|
||||
}, [filterOptions, task.comment, task.command, task.display_params, task.operator.username]);
|
||||
// marginLeft: (indentLevel * 10) + "px"
|
||||
return (
|
||||
shouldDisplay ? (
|
||||
<div style={{}}>
|
||||
<TaskLabelConsole me={me} task={task} newlyIssuedTasks={newlyIssuedTasks} />
|
||||
{
|
||||
taskingData.map( (tsk) => (
|
||||
<TaskRowConsole key={"taskrow: " + tsk.id} me={me} task={tsk}
|
||||
filterOptions={filterOptions} indentLevel={indentLevel+1}/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
) : null
|
||||
)
|
||||
}
|
||||
export const ColoredTaskLabelConsole = ({task, theme, me, taskDivID, onClick, displayChildren, toggleDisplayChildren, expanded }) => {
|
||||
const initialHideUsernameValue = GetMythicSetting({setting_name: "hideUsernames", default_value: operatorSettingDefaults.hideUsernames});
|
||||
const initialTaskTimestampDisplayField = GetMythicSetting({setting_name: "taskTimestampDisplayField", default_value: operatorSettingDefaults.taskTimestampDisplayField});
|
||||
const displayTimestamp = task[initialTaskTimestampDisplayField] ? task[initialTaskTimestampDisplayField] : task.timestamp;
|
||||
const initialShowHostnameValue = GetMythicSetting({setting_name: "showHostname", default_value: operatorSettingDefaults.showHostname});
|
||||
const initialShowIPValue = GetMythicSetting({setting_name: "showIP", default_value: operatorSettingDefaults.showIP});
|
||||
const [openKillTaskButton, setOpenKillTaskButton] = React.useState({open: false});
|
||||
const preventPropagation = (e) => {
|
||||
e.stopPropagation();
|
||||
//e.preventDefault();
|
||||
}
|
||||
const onClickKillIcon = (e, open) => {
|
||||
if(e){
|
||||
e.stopPropagation();
|
||||
}
|
||||
setOpenKillTaskButton({open: open});
|
||||
}
|
||||
const [themeColor, setThemeColor] = React.useState(theme.palette.info.main);
|
||||
useEffect( () => {
|
||||
if(task.status.toLowerCase().includes("error")){
|
||||
setThemeColor(theme.palette.error.main);
|
||||
}else if(task.status.toLowerCase() === "cleared"){
|
||||
setThemeColor(theme.palette.warning.main);
|
||||
}else if(task.status === "submitted"){
|
||||
setThemeColor(theme.palette.info.main);
|
||||
}else if(task.opsec_pre_blocked && !task.opsec_pre_bypassed){
|
||||
setThemeColor(theme.palette.warning.main);
|
||||
}else if(task.opsec_post_blocked && !task.opsec_post_bypassed){
|
||||
setThemeColor(theme.palette.warning.main);
|
||||
}else if(task.status.toLowerCase().includes("processing")){
|
||||
setThemeColor(theme.palette.warning.main);
|
||||
}else if(task.status === "completed" || (task.status === "success" && task.completed)){
|
||||
setThemeColor(theme.palette.success.main);
|
||||
}else{
|
||||
setThemeColor(theme.palette.info.main);
|
||||
}
|
||||
}, [task.status, task.completed])
|
||||
return (
|
||||
<ColoredTaskDisplayConsole task={task} theme={theme} expanded={expanded}>
|
||||
<div id={taskDivID} style={{ width: "100%" }}>
|
||||
<Typography sx={{ color: theme.taskPromptTextColor, display: "flex", alignItems: "center" }} style={{ fontFamily: "monospace" }}>
|
||||
<span style={{ fontFamily: "monospace" }}>┌──[</span>
|
||||
<AccessTimeIcon fontSize="small" style={{ margin: "0 3px 0 0px", verticalAlign: "middle" }} />
|
||||
{toLocalTimeShort(displayTimestamp, me?.user?.view_utc_time || false)}
|
||||
{"]"}
|
||||
{!initialHideUsernameValue &&
|
||||
<>
|
||||
{"-["}
|
||||
<PersonOutlineIcon fontSize="small" style={{ margin: "0 3px 0 0px", verticalAlign: "middle" }} />
|
||||
{task.operator.username}
|
||||
{"]"}
|
||||
</>
|
||||
}
|
||||
{"-["}
|
||||
<NumbersIcon fontSize="small" style={{ margin: "0 3px 0 0px", verticalAlign: "middle" }} />
|
||||
<MythicStyledTooltip title={"View Task in separate page"} >
|
||||
<Link style={{wordBreak: "break-all", color: theme.taskPromptTextColor,}} underline={"always"} target={"_blank"}
|
||||
href={"/new/task/" + task.display_id}>T-{task.display_id}</Link>
|
||||
</MythicStyledTooltip>
|
||||
{!task.completed && task.status_timestamp_processing &&
|
||||
<>
|
||||
<MythicStyledTooltip title={"Task the agent to kill this task"} >
|
||||
<FontAwesomeIcon size={"sm"} icon={faSkullCrossbones} onClick={(e) => onClickKillIcon(e, true)}
|
||||
style={{cursor: "pointer", height: "15px", marginLeft: "5px"}} />
|
||||
</MythicStyledTooltip>
|
||||
</>
|
||||
}
|
||||
{"]"}
|
||||
{initialShowHostnameValue ? (
|
||||
<>
|
||||
{"-["}
|
||||
<ComputerIcon fontSize="small" style={{ margin: "0 3px 0 0px", verticalAlign: "middle" }} />
|
||||
{task.callback.host}
|
||||
{"]"}
|
||||
</>) : null}
|
||||
{initialShowIPValue ? (
|
||||
<>
|
||||
{"-["}
|
||||
<PublicIcon fontSize="small" style={{ margin: "0 3px 0 0px", verticalAlign: "middle" }} />
|
||||
{JSON.parse(task.callback.ip)[0]}
|
||||
{"]"}
|
||||
</> ) : null}
|
||||
{(task.opsec_pre_blocked && !task.opsec_pre_bypassed) ? (
|
||||
<>
|
||||
{"-["}
|
||||
<WarningAmberIcon fontSize="small" style={{ margin: "0 3px 0 0px", verticalAlign: "middle", color: theme.palette.warning.main }} />
|
||||
{"OPSEC BLOCKED (PRE)]"}
|
||||
</>) : null}
|
||||
{(task.opsec_post_blocked && !task.opsec_post_bypassed) ? (
|
||||
<>
|
||||
{"-["}
|
||||
<WarningAmberIcon fontSize="small" style={{ margin: "0 3px 0 0px", verticalAlign: "middle", color: theme.palette.warning.main }} />
|
||||
{"OPSEC BLOCKED (POST)]"}
|
||||
</>) : null}
|
||||
{")"}
|
||||
</Typography>
|
||||
<MythicStyledTooltip title={task.status}>
|
||||
<span style={{ fontFamily: "monospace", marginRight: 2, color: theme.taskPromptTextColor }}>└─</span>
|
||||
<b style={{ fontFamily: "monospace", color: themeColor, fontWeight: 600 }}>{">_"}</b>{" "}
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip
|
||||
title={(task?.command?.cmd || task.command_name) + " " + task.display_params}
|
||||
maxWidth="calc(80vw)"
|
||||
enterDelay={2000}
|
||||
placement="top">
|
||||
<Typography
|
||||
sx={{ fontSize: 15, display: "flex", alignItems: "center", marginLeft: 0}}
|
||||
style={{ fontFamily: "monospace" }}>
|
||||
<span style={{ marginLeft: 4, color: theme.taskPromptCommandTextColor }}>
|
||||
<b>{(task?.command?.cmd || task.command_name)}</b> {task.display_params}
|
||||
</span>
|
||||
</Typography>
|
||||
</MythicStyledTooltip>
|
||||
</div>
|
||||
{openKillTaskButton.open &&
|
||||
<TaskFromUIButton ui_feature={"task:job_kill"}
|
||||
callback_id={task.callback?.id}
|
||||
display_id={task.callback?.display_id}
|
||||
parameters={task.agent_task_id}
|
||||
openDialog={false}
|
||||
getConfirmation={true}
|
||||
acceptText={"KILL JOB"}
|
||||
selectCallback={false}
|
||||
onTasked={({tasked}) => onClickKillIcon(null, false)}/>
|
||||
}
|
||||
</ColoredTaskDisplayConsole>
|
||||
);
|
||||
}
|
||||
const TaskLabelConsole = ({task, me}) => {
|
||||
const theme = useTheme();
|
||||
useLayoutEffect( () => {
|
||||
@@ -932,9 +1095,10 @@ const TaskLabelConsole = ({task, me}) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledPaper className={classes.root + " no-box-shadow"} elevation={5} style={{marginRight: 0}} id={`taskHeader-${task.id}`}>
|
||||
<ColoredTaskLabel theme={theme} task={task} me={me} taskDivID={`scrolltotaskconsole${task.id}`} expanded={true}/>
|
||||
<StyledPaper className={classes.root + " no-box-shadow no-border"} elevation={5} style={{marginRight: 0, marginBottom: "5px"}} id={`taskHeader-${task.id}`}>
|
||||
<ColoredTaskLabelConsole theme={theme} task={task} me={me} taskDivID={`scrolltotaskconsole${task.id}`} expanded={true}/>
|
||||
<TaskDisplayContainerConsole me={me} task={task} />
|
||||
<div style={{borderBottom: "0px dashed grey", width: "100%", height: "5px", marginTop: "5px"}}/>
|
||||
</StyledPaper>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,11 @@ import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen';
|
||||
import CodeOffIcon from '@mui/icons-material/CodeOff';
|
||||
import SettingsTwoToneIcon from '@mui/icons-material/SettingsTwoTone';
|
||||
import {b64DecodeUnicode} from './ResponseDisplay';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {EventTriggerContextSelectDialog} from "../Eventing/EventTriggerContextSelect";
|
||||
import PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwoTone';
|
||||
|
||||
const ReissueTaskMutationGQL = gql`
|
||||
mutation reissueTaskMutation($task_id: Int!){
|
||||
@@ -131,7 +136,7 @@ export const TaskDisplayContainerFlat = ({task, me}) => {
|
||||
}, [searchOutput]);
|
||||
|
||||
return (
|
||||
<div style={{ height: "100%", position: "relative", display: "flex", flexDirection: "column", overflowY: "auto", }}>
|
||||
<div style={{ height: "100%", position: "relative", display: "flex", overflowY: "auto", justifyContent: "flex-end" }}>
|
||||
<ResponseDisplay
|
||||
key={task.id}
|
||||
task={task}
|
||||
@@ -143,7 +148,7 @@ export const TaskDisplayContainerFlat = ({task, me}) => {
|
||||
expand={true}
|
||||
responseRef={responseRef}
|
||||
/>
|
||||
<SpeedDialDisplayGeneric toggleViewBrowserScript={toggleViewBrowserScript}
|
||||
<SideDisplayGeneric toggleViewBrowserScript={toggleViewBrowserScript}
|
||||
toggleSelectAllOutput={toggleSelectAllOutput}
|
||||
toggleOpenSearch={toggleOpenSearch}
|
||||
taskData={task}
|
||||
@@ -160,33 +165,13 @@ export const TaskDisplayContainerConsole = ({task, me}) => {
|
||||
const [viewBrowserScript, setViewBrowserScript] = React.useState(true);
|
||||
const [commandID, setCommandID] = React.useState(task?.command?.id || 0);
|
||||
const [searchOutput, setSearchOutput] = React.useState(false);
|
||||
const [selectAllOutput, setSelectAllOutput] = React.useState(false);
|
||||
const responseRef = React.useRef(null);
|
||||
useEffect( () => {
|
||||
setCommandID(task.command === null ? 0 : task.command.id);
|
||||
}, [task.command?.id]);
|
||||
const toggleViewBrowserScript = React.useCallback( () => {
|
||||
setViewBrowserScript(!viewBrowserScript);
|
||||
}, [viewBrowserScript]);
|
||||
const toggleSelectAllOutput = React.useCallback( () => {
|
||||
setSelectAllOutput(!selectAllOutput);
|
||||
}, [selectAllOutput]);
|
||||
const toggleOpenSearch = React.useCallback( () => {
|
||||
setSearchOutput(!searchOutput);
|
||||
}, [searchOutput]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SpeedDialDisplayGeneric toggleViewBrowserScript={toggleViewBrowserScript}
|
||||
toggleSelectAllOutput={toggleSelectAllOutput}
|
||||
toggleOpenSearch={toggleOpenSearch}
|
||||
taskData={task}
|
||||
me={me}
|
||||
viewBrowserScript={viewBrowserScript}
|
||||
responseRef={responseRef}
|
||||
style={{ bottom: "25px", right: "4px", zIndex: 2, height: "0px", position: "relative", display: "flex", flexDirection: "row-reverse"}}
|
||||
fabStyle={{ }}
|
||||
viewAllOutput={true}/>
|
||||
<ResponseDisplayConsole
|
||||
task={task}
|
||||
me={me}
|
||||
@@ -199,11 +184,278 @@ export const TaskDisplayContainerConsole = ({task, me}) => {
|
||||
);
|
||||
}
|
||||
|
||||
const SideDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput,
|
||||
toggleOpenSearch, taskData, viewAllOutput, me,
|
||||
responseRef, viewBrowserScript}) => {
|
||||
const theme = useTheme();
|
||||
const [task, setTask] = React.useState(taskData || {});
|
||||
const eventingDataRef = React.useRef({name: "", value: 0});
|
||||
const [openEventingDialog, setOpenEventingDialog] = React.useState(false);
|
||||
const [openTaskTagDialog, setOpenTaskTagDialog] = React.useState(false);
|
||||
const [openCommentDialog, setOpenCommentDialog] = React.useState(false);
|
||||
const [openParametersDialog, setOpenParametersDialog] = React.useState(false);
|
||||
const [openTokenDialog, setOpenTokenDialog] = React.useState(false);
|
||||
const [openStdoutStderrDialog, setOpenStdoutStderrDialog] = React.useState(false);
|
||||
const [openOpsecDialog, setOpenOpsecDialog] = React.useState({open: false, view: "pre"});
|
||||
const [downloadResponses] = useLazyQuery(getAllResponsesLazyQuery, {
|
||||
fetchPolicy: "network-only",
|
||||
onCompleted: (data) => {
|
||||
const output = data.response.reduce( (prev, cur) => {
|
||||
return prev + b64DecodeUnicode(cur.response);
|
||||
}, b64DecodeUnicode(""));
|
||||
downloadFileFromMemory(output, "task_" + task.id + ".txt");
|
||||
},
|
||||
onError: (data) => {
|
||||
|
||||
}
|
||||
});
|
||||
React.useEffect( () => {
|
||||
setTask(taskData);
|
||||
}, [taskData.id, taskData.token, taskData.original_params, taskData.opsec_pre_blocked, taskData.opsec_pre_bypassed, taskData.opsec_post_blocked, taskData.opsec_post_bypassed])
|
||||
const onDownloadResponses = () => {
|
||||
downloadResponses({variables: {task_id: task.id}});
|
||||
};
|
||||
const copyToClipboard = () => {
|
||||
let command = task?.command?.cmd || task.command_name;
|
||||
let result = copyStringToClipboard(command + " " + task.original_params);
|
||||
if(result){
|
||||
snackActions.success("Copied text!");
|
||||
}else{
|
||||
snackActions.error("Failed to copy text");
|
||||
}
|
||||
};
|
||||
const [reissueTask] = useMutation(ReissueTaskMutationGQL, {
|
||||
onCompleted: data => {
|
||||
if(data.reissue_task.status === "success"){
|
||||
snackActions.success("Successfully re-issued task to Mythic");
|
||||
}else{
|
||||
snackActions.error("Failed to re-issue task to Mythic: " + data.reissue_task.error);
|
||||
}
|
||||
},
|
||||
onError: data => {
|
||||
console.log(data);
|
||||
snackActions.error("Failed to re-issue task: " + data);
|
||||
}
|
||||
});
|
||||
const [reissueTaskHandler] = useMutation(ReissueTaskHandlerMutationGQL, {
|
||||
onCompleted: data => {
|
||||
if(data.reissue_task_handler.status === "success"){
|
||||
snackActions.success("Successfully resubmitted task for handling");
|
||||
}else{
|
||||
snackActions.warning("Failed to resubmit task for handling: " + data.reissue_task_handler.error);
|
||||
}
|
||||
|
||||
},
|
||||
onError: data => {
|
||||
console.log(data);
|
||||
snackActions.error("Error resubmitting task for handling: " + data);
|
||||
}
|
||||
});
|
||||
const onDownloadImageClickPng = () => {
|
||||
// we calculate a transform for the nodes so that all nodes are visible
|
||||
// we then overwrite the transform of the `.react-flow__viewport` element
|
||||
// with the style option of the html-to-image library
|
||||
snackActions.info("Saving image to png...");
|
||||
(async () => {
|
||||
const canvas = await html2canvas(responseRef.current);
|
||||
const image = canvas.toDataURL("image/png", 1.0);
|
||||
const fakeLink = window.document.createElement("a");
|
||||
fakeLink.style = "display:none;";
|
||||
fakeLink.download = "task_output.png";
|
||||
|
||||
fakeLink.href = image;
|
||||
|
||||
document.body.appendChild(fakeLink);
|
||||
fakeLink.click();
|
||||
document.body.removeChild(fakeLink);
|
||||
|
||||
fakeLink.remove();
|
||||
|
||||
})();
|
||||
};
|
||||
const onReissueTask = () => {
|
||||
reissueTask({variables: {task_id: task.id}});
|
||||
}
|
||||
const onReissueTaskHandler = () => {
|
||||
reissueTaskHandler({variables: {task_id: task.id}});
|
||||
}
|
||||
const onTriggerEventing = () => {
|
||||
eventingDataRef.current = {
|
||||
name: "task_id",
|
||||
value: task.id
|
||||
};
|
||||
setOpenEventingDialog(true);
|
||||
}
|
||||
return (
|
||||
<div style={{height: "100%"}}>
|
||||
{openTaskTagDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openTaskTagDialog}
|
||||
onClose={()=>{setOpenTaskTagDialog(false);}}
|
||||
innerDialog={<ViewEditTagsDialog me={me} target_object={"task_id"} target_object_id={task.id} onClose={()=>{setOpenTaskTagDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
{openCommentDialog && <MythicDialog fullWidth={true} maxWidth="md" open={openCommentDialog}
|
||||
onClose={()=>{setOpenCommentDialog(false);}}
|
||||
innerDialog={<TaskCommentDialog task_id={task.id} onClose={()=>{setOpenCommentDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
{openParametersDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openParametersDialog}
|
||||
onClose={()=>{setOpenParametersDialog(false);}}
|
||||
innerDialog={<TaskViewParametersDialog task_id={task.id} onClose={()=>{setOpenParametersDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
{openTokenDialog && <MythicDialog fullWidth={true} maxWidth="md" open={openTokenDialog}
|
||||
onClose={()=>{setOpenTokenDialog(false);}}
|
||||
innerDialog={<TaskTokenDialog token_id={task.token === undefined ? 0 : task.token.id} onClose={()=>{setOpenTokenDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
{openOpsecDialog.open && <MythicDialog fullWidth={true} maxWidth="lg" open={openOpsecDialog.open}
|
||||
onClose={()=>{setOpenOpsecDialog({...openOpsecDialog, open: false});}}
|
||||
innerDialog={<TaskOpsecDialog task_id={task.id} view={openOpsecDialog.view} onClose={()=>{setOpenOpsecDialog({...openOpsecDialog, open: false});}} />}
|
||||
/>
|
||||
}
|
||||
|
||||
{openStdoutStderrDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openStdoutStderrDialog}
|
||||
onClose={()=>{setOpenStdoutStderrDialog(false);}}
|
||||
innerDialog={<TaskViewStdoutStderrDialog task_id={task.id} onClose={()=>{setOpenStdoutStderrDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
{openEventingDialog &&
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
maxWidth="xl"
|
||||
open={openEventingDialog}
|
||||
onClose={() => {setOpenEventingDialog(false);}}
|
||||
innerDialog={
|
||||
<EventTriggerContextSelectDialog onClose={() => {setOpenEventingDialog(false);}}
|
||||
triggerContext={eventingDataRef.current}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
<Paper elevation={5} style={{width: "30px", display: "flex", flexDirection: "column", alignItems: "center", overflow: "hidden",
|
||||
backgroundColor: "transparent", height: "100%"}}>
|
||||
<MythicStyledTooltip title={"Toggle BrowserScript"} >
|
||||
<IconButton onClick={toggleViewBrowserScript} style={{paddingBottom: "3px"}}>
|
||||
{viewBrowserScript ? <CodeOffIcon color={"error"} /> : <CodeIcon color={"success"}/>}
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={viewAllOutput ? "View Paginated Output" : "View All Output"}>
|
||||
<IconButton onClick={toggleSelectAllOutput} style={{paddingBottom: "3px"}}>
|
||||
{viewAllOutput ? <CloseFullscreenIcon color={"error"} /> : <FontAwesomeIcon style={{color: theme.palette.success.main}} icon={faExpandArrowsAlt} size="sm" />}
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Search Output"}>
|
||||
<IconButton onClick={toggleOpenSearch} style={{paddingBottom: "3px"}}>
|
||||
<SearchIcon color={"info"} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Download output"}>
|
||||
<IconButton onClick={onDownloadResponses} style={{paddingBottom: "3px"}}>
|
||||
<GetAppIcon color={"success"}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Download screenshot of output"}>
|
||||
<IconButton onClick={onDownloadImageClickPng} style={{paddingBottom: "3px"}}>
|
||||
<InsertPhotoIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Edit Tags"}>
|
||||
<IconButton onClick={()=>{setOpenTaskTagDialog(true)}} style={{paddingBottom: "3px"}}>
|
||||
<LocalOfferOutlinedIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Open Task in New Window"}>
|
||||
<IconButton onClick={()=> {window.open('/new/task/' + task.display_id, "_blank")}} style={{paddingBottom: "3px"}}>
|
||||
<FontAwesomeIcon icon={faExternalLinkAlt} size="sm" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Copy original params to clipboard"}>
|
||||
<IconButton onClick={copyToClipboard} style={{paddingBottom: "3px"}}>
|
||||
<FileCopyOutlinedIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Edit Comment"}>
|
||||
<IconButton onClick={()=>{setOpenCommentDialog(true)}} style={{paddingBottom: "3px"}}>
|
||||
<RateReviewOutlinedIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"View All Parameters And Timestamps"}>
|
||||
<IconButton onClick={()=>{setOpenParametersDialog(true);}} style={{paddingBottom: "3px"}}>
|
||||
<KeyboardIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"View Stdout/Stderr of Task"}>
|
||||
<IconButton onClick={()=>{setOpenStdoutStderrDialog(true);}} style={{paddingBottom: "3px"}}>
|
||||
<FontAwesomeIcon style={{color: theme.palette.error.main}} icon={faExclamationTriangle} size="sm" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Trigger Eventing Based on Task"}>
|
||||
<IconButton onClick={()=>{onTriggerEventing();}} style={{paddingBottom: "3px"}}>
|
||||
<PlayCircleFilledTwoToneIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
{task.opsec_pre_blocked === null ? null : ( task.opsec_pre_bypassed === false ? (
|
||||
<MythicStyledTooltip title={"Submit OPSEC PreCheck Bypass Request"}>
|
||||
<IconButton onClick={()=>{setOpenOpsecDialog({open: true, view: "pre"})}} style={{paddingBottom: "3px"}}>
|
||||
<LockIcon style={{color: theme.palette.error.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
): (
|
||||
<MythicStyledTooltip title={"View OPSEC PreCheck Data"}>
|
||||
<IconButton onClick={()=>{setOpenOpsecDialog({open: true, view: "pre"})}} style={{paddingBottom: "3px"}}>
|
||||
<LockOpenIcon style={{color: theme.palette.success.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
)
|
||||
)
|
||||
}
|
||||
{task.opsec_post_blocked === null ? null : ( task.opsec_post_bypassed === false ? (
|
||||
<MythicStyledTooltip title={"Submit OPSEC PostCheck Bypass Request"}>
|
||||
<IconButton onClick={()=>{setOpenOpsecDialog({open: true, view: "post"})}} style={{paddingBottom: "3px"}}>
|
||||
<LockIcon style={{color: theme.palette.error.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
): (
|
||||
<MythicStyledTooltip title={"View OPSEC PostCheck Data"}>
|
||||
<IconButton onClick={()=>{setOpenOpsecDialog({open: true, view: "post"})}} style={{paddingBottom: "3px"}}>
|
||||
<LockOpenIcon style={{color: theme.palette.success.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
)
|
||||
)
|
||||
}
|
||||
{task.token === null ? null : (
|
||||
<MythicStyledTooltip title={"View Token Information"}>
|
||||
<IconButton onClick={()=>{setOpenTokenDialog(true)}} style={{paddingBottom: "3px"}}>
|
||||
<ConfirmationNumberIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
)}
|
||||
{task.status.toLowerCase().includes("error: container") &&
|
||||
<MythicStyledTooltip title={"Resubmit Tasking"}>
|
||||
<IconButton onClick={onReissueTask} style={{paddingBottom: "3px"}}>
|
||||
<ReplayIcon style={{color: theme.palette.warning.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
{task.status.toLowerCase().includes("error: task") &&
|
||||
<MythicStyledTooltip title={"Resubmit Task Handler"}>
|
||||
<IconButton onClick={onReissueTaskHandler} style={{paddingBottom: "3px"}}>
|
||||
<ReplayIcon style={{color: theme.palette.warning.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
</Paper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput,
|
||||
toggleOpenSearch, taskData, viewAllOutput, me,
|
||||
responseRef, style, fabStyle, viewBrowserScript}) => {
|
||||
const theme = useTheme();
|
||||
const [task, setTask] = React.useState(taskData || {});
|
||||
const eventingDataRef = React.useRef({name: "", value: 0});
|
||||
const [openEventingDialog, setOpenEventingDialog] = React.useState(false);
|
||||
const [openSpeedDial, setOpenSpeedDial] = React.useState(false);
|
||||
const [openTaskTagDialog, setOpenTaskTagDialog] = React.useState(false);
|
||||
const [openCommentDialog, setOpenCommentDialog] = React.useState(false);
|
||||
@@ -295,45 +547,60 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput
|
||||
const onReissueTaskHandler = () => {
|
||||
reissueTaskHandler({variables: {task_id: task.id}});
|
||||
}
|
||||
const onTriggerEventing = () => {
|
||||
eventingDataRef.current = {
|
||||
name: "task_id",
|
||||
value: task.id
|
||||
};
|
||||
setOpenEventingDialog(true);
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Backdrop open={openSpeedDial} onClick={()=>{setOpenSpeedDial(false);}} style={{zIndex: 2, position: "absolute"}}/>
|
||||
{openTaskTagDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="lg" open={openTaskTagDialog}
|
||||
{openTaskTagDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openTaskTagDialog}
|
||||
onClose={()=>{setOpenTaskTagDialog(false);}}
|
||||
innerDialog={<ViewEditTagsDialog me={me} target_object={"task_id"} target_object_id={task.id} onClose={()=>{setOpenTaskTagDialog(false);}} />}
|
||||
/>) : null}
|
||||
{openCommentDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="lg" open={openCommentDialog}
|
||||
/>
|
||||
}
|
||||
{openCommentDialog && <MythicDialog fullWidth={true} maxWidth="md" open={openCommentDialog}
|
||||
onClose={()=>{setOpenCommentDialog(false);}}
|
||||
innerDialog={<TaskCommentDialog task_id={task.id} onClose={()=>{setOpenCommentDialog(false);}} />}
|
||||
/>) : null
|
||||
/>
|
||||
}
|
||||
{openParametersDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="lg" open={openParametersDialog}
|
||||
{openParametersDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openParametersDialog}
|
||||
onClose={()=>{setOpenParametersDialog(false);}}
|
||||
innerDialog={<TaskViewParametersDialog task_id={task.id} onClose={()=>{setOpenParametersDialog(false);}} />}
|
||||
/>) : null
|
||||
/>
|
||||
}
|
||||
{openTokenDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="md" open={openTokenDialog}
|
||||
{openTokenDialog && <MythicDialog fullWidth={true} maxWidth="md" open={openTokenDialog}
|
||||
onClose={()=>{setOpenTokenDialog(false);}}
|
||||
innerDialog={<TaskTokenDialog token_id={task.token === undefined ? 0 : task.token.id} onClose={()=>{setOpenTokenDialog(false);}} />}
|
||||
/>) : null
|
||||
/>
|
||||
}
|
||||
{openOpsecDialog.open ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="md" open={openOpsecDialog.open}
|
||||
{openOpsecDialog.open && <MythicDialog fullWidth={true} maxWidth="lg" open={openOpsecDialog.open}
|
||||
onClose={()=>{setOpenOpsecDialog({...openOpsecDialog, open: false});}}
|
||||
innerDialog={<TaskOpsecDialog task_id={task.id} view={openOpsecDialog.view} onClose={()=>{setOpenOpsecDialog({...openOpsecDialog, open: false});}} />}
|
||||
/>) : null
|
||||
/>
|
||||
}
|
||||
|
||||
{openStdoutStderrDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="lg" open={openStdoutStderrDialog}
|
||||
{openStdoutStderrDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openStdoutStderrDialog}
|
||||
onClose={()=>{setOpenStdoutStderrDialog(false);}}
|
||||
innerDialog={<TaskViewStdoutStderrDialog task_id={task.id} onClose={()=>{setOpenStdoutStderrDialog(false);}} />}
|
||||
/>) : null
|
||||
/>
|
||||
}
|
||||
{openEventingDialog &&
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
maxWidth="xl"
|
||||
open={openEventingDialog}
|
||||
onClose={() => {setOpenEventingDialog(false);}}
|
||||
innerDialog={
|
||||
<EventTriggerContextSelectDialog onClose={() => {setOpenEventingDialog(false);}}
|
||||
triggerContext={eventingDataRef.current}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
<SpeedDial
|
||||
ariaLabel="Task Speeddial"
|
||||
icon={<SettingsTwoToneIcon fontSize={"large"} />}
|
||||
@@ -420,6 +687,13 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput
|
||||
tooltipTitle={"View Stdout/Stderr of Task"}
|
||||
onClick={()=>{setOpenStdoutStderrDialog(true);setOpenSpeedDial(false);}}
|
||||
/>
|
||||
<SpeedDialAction
|
||||
icon={<PlayCircleFilledTwoToneIcon />}
|
||||
arrow
|
||||
tooltipPlacement={"top"}
|
||||
tooltipTitle={"Trigger Eventing Based on Task"}
|
||||
onClick={()=>{onTriggerEventing();setOpenSpeedDial(false);}}
|
||||
/>
|
||||
{task.opsec_pre_blocked === null ? null : ( task.opsec_pre_bypassed === false ? (
|
||||
<SpeedDialAction
|
||||
icon={<LockIcon style={{color: theme.palette.error.main}}/>}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {Backdrop, CircularProgress} from '@mui/material';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import {b64DecodeUnicode} from './ResponseDisplay';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import {MythicDraggableDialogTitle} from "../../MythicComponents/MythicDraggableDialogTitle";
|
||||
|
||||
//if we need to get all the loaded commands for the callback and filter, use this
|
||||
const GetLoadedCommandsQuery = gql`
|
||||
@@ -105,7 +106,7 @@ query getAllEdgesQuery($callback_id: Int!){
|
||||
// get all payloads query
|
||||
const getAllPayloadsQuery = gql`
|
||||
query getAllPayloadsQuery($operation_id: Int!){
|
||||
payload(where: {deleted: {_eq: false}, build_phase: {_eq: "success"}, operation_id: {_eq: $operation_id}}) {
|
||||
payload(where: {deleted: {_eq: false}, build_phase: {_eq: "success"}, operation_id: {_eq: $operation_id}}, order_by: {id: desc}) {
|
||||
id
|
||||
description
|
||||
uuid
|
||||
@@ -141,7 +142,7 @@ query getAllPayloadsQuery($operation_id: Int!){
|
||||
// get all payloads on hosts
|
||||
const getAllPayloadsOnHostsQuery = gql`
|
||||
query getAllPayloadsOnHostsQuery($operation_id: Int!){
|
||||
payloadonhost(where: {deleted: {_eq: false}, operation_id: {_eq: $operation_id}, payload: {c2profileparametersinstances: {c2profile: {is_p2p: {_eq: true}}}}}) {
|
||||
payloadonhost(where: {deleted: {_eq: false}, operation_id: {_eq: $operation_id}, payload: {c2profileparametersinstances: {c2profile: {is_p2p: {_eq: true}}}}}, order_by: {id: desc}) {
|
||||
host
|
||||
id
|
||||
payload {
|
||||
@@ -256,6 +257,7 @@ query getCommandQuery($id: Int!){
|
||||
parameter_group_name
|
||||
display_name
|
||||
cli_name
|
||||
verifier_regex
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,7 +265,7 @@ query getCommandQuery($id: Int!){
|
||||
// use this to get all the credentials for the command we're trying to execute
|
||||
const getCredentialsQuery = gql`
|
||||
query getCredentialsQuery($operation_id: Int!){
|
||||
credential(where: {deleted: {_eq: false}, operation_id: {_eq: $operation_id}}){
|
||||
credential(where: {deleted: {_eq: false}, operation_id: {_eq: $operation_id}}, order_by: {id: desc}){
|
||||
account
|
||||
comment
|
||||
credential_text
|
||||
@@ -383,9 +385,8 @@ export function TaskParametersDialog(props) {
|
||||
const addedCredential = (credential) => {
|
||||
getAllCredentials({variables: {operation_id: props.operation_id}});
|
||||
}
|
||||
const intersect = (a, b) => {
|
||||
let setB = new Set(b);
|
||||
return [...new Set(a)].filter(x => setB.has(x));
|
||||
const removedCredential = (credential) => {
|
||||
getAllCredentials({variables: {operation_id: props.operation_id}});
|
||||
}
|
||||
const setSubmenuOpenPreventTasking = (open) => {
|
||||
setSubmenuOpenPreventTask(open);
|
||||
@@ -516,8 +517,16 @@ export function TaskParametersDialog(props) {
|
||||
let match = true;
|
||||
let cmd_attributes = c.attributes;
|
||||
for(const [key, value] of Object.entries(filter)){
|
||||
if(key === "supported_os" && value.length > 0){
|
||||
if(intersect(value, cmd_attributes[key]).length === 0){
|
||||
if(cmd_attributes[key] === undefined){continue}
|
||||
if(Array.isArray(cmd_attributes[key])){
|
||||
if(key === 'supported_os' && cmd_attributes[key].length === 0){
|
||||
continue;
|
||||
}
|
||||
if(!cmd_attributes[key].includes(value)){
|
||||
match = false;
|
||||
}
|
||||
} else {
|
||||
if(cmd_attributes[key] !== value){
|
||||
match = false;
|
||||
}
|
||||
}
|
||||
@@ -540,8 +549,16 @@ export function TaskParametersDialog(props) {
|
||||
let match = true;
|
||||
let cmd_attributes = c.command.attributes;
|
||||
for(const [key, value] of Object.entries(filter)){
|
||||
if(key === "supported_os" && value.length > 0){
|
||||
if(intersect(value, cmd_attributes[key]).length === 0){
|
||||
if(cmd_attributes[key] === undefined){continue}
|
||||
if(Array.isArray(cmd_attributes[key])){
|
||||
if(key === 'supported_os' && cmd_attributes[key].length === 0){
|
||||
continue;
|
||||
}
|
||||
if(!cmd_attributes[key].includes(value)){
|
||||
match = false;
|
||||
}
|
||||
} else {
|
||||
if(cmd_attributes[key] !== value){
|
||||
match = false;
|
||||
}
|
||||
}
|
||||
@@ -961,7 +978,7 @@ export function TaskParametersDialog(props) {
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">{commandInfo.cmd}'s Parameters</DialogTitle>
|
||||
<MythicDraggableDialogTitle>{commandInfo.cmd}'s Parameters</MythicDraggableDialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}}>
|
||||
<CircularProgress color="inherit" />
|
||||
@@ -1007,7 +1024,7 @@ export function TaskParametersDialog(props) {
|
||||
callback_id={props.callback_id}
|
||||
onAgentConnectAddNewPayloadOnHost={onAgentConnectAddNewPayloadOnHost}
|
||||
onAgentConnectRemovePayloadOnHost={onAgentConnectRemovePayloadOnHost}
|
||||
addedCredential={addedCredential}
|
||||
addedCredential={addedCredential} removedCredential={removedCredential}
|
||||
setSubmenuOpenPreventTasking={setSubmenuOpenPreventTasking}
|
||||
getOtherParameters={getOtherParameters}
|
||||
/>
|
||||
|
||||
@@ -26,20 +26,21 @@ import {CircularProgress} from '@mui/material';
|
||||
import MythicStyledTableCell from '../../MythicComponents/MythicTableCell';
|
||||
import {MythicFileContext} from "../../MythicComponents/MythicFileContext";
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import {updateCredentialDeleted} from "../Search/CredentialTable";
|
||||
import CloudUploadTwoToneIcon from '@mui/icons-material/CloudUploadTwoTone';
|
||||
|
||||
export const getDynamicQueryParams = gql`
|
||||
export const getDynamicQueryParamsString = `
|
||||
mutation getDynamicParamsMutation($callback: Int!, $command: String!, $payload_type: String!, $parameter_name: String!, $other_parameters: jsonb){
|
||||
dynamic_query_function(callback: $callback, command: $command, payload_type: $payload_type, parameter_name: $parameter_name, other_parameters: $other_parameters){
|
||||
status
|
||||
error
|
||||
choices
|
||||
parameter_name
|
||||
complex_choices {
|
||||
value
|
||||
display_value
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
export const getDynamicQueryParams = gql`
|
||||
${getDynamicQueryParamsString}
|
||||
`;
|
||||
const parseTypedArrayMutation = gql`
|
||||
mutation parseTypedArrayMutation($callback: Int!, $command: String!, $payload_type: String!, $parameter_name: String!, $input_array: [String!]!){
|
||||
@@ -84,13 +85,24 @@ query getCredential($id: Int!){
|
||||
}
|
||||
`;
|
||||
|
||||
const arraysAreDifferent = (a, b) => {
|
||||
if(a.length !== b.length){
|
||||
return true
|
||||
}
|
||||
for(let i = 0; i < a.length; i++){
|
||||
if(a[i] !== b[i]){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
export function TaskParametersDialogRow(props){
|
||||
const [value, setValue] = React.useState('');
|
||||
const theme = useTheme();
|
||||
const currentParameterGroup = React.useRef(props.parameterGroupName);
|
||||
const [ChoiceOptions, setChoiceOptions] = React.useState([]);
|
||||
const [boolValue, setBoolValue] = React.useState(false);
|
||||
const [arrayValue, setArrayValue] = React.useState([""]);
|
||||
const [arrayValue, setArrayValue] = React.useState([]);
|
||||
const [typedArrayValue, setTypedArrayValue] = React.useState([]);
|
||||
const [chooseMultipleValue, setChooseMultipleValue] = React.useState([]);
|
||||
const [chooseOneCustomValue, setChooseOneCustomValue] = React.useState("");
|
||||
@@ -116,7 +128,9 @@ export function TaskParametersDialogRow(props){
|
||||
if(data.dynamic_query_function.status === "success"){
|
||||
try{
|
||||
let choicesInUse = [];
|
||||
if (data.dynamic_query_function.complex_choices !== null && data.dynamic_query_function.complex_choices.length > 0) {
|
||||
if (data.dynamic_query_function.complex_choices !== null &&
|
||||
data.dynamic_query_function.complex_choices !== undefined &&
|
||||
data.dynamic_query_function.complex_choices.length > 0) {
|
||||
usingDynamicParamComplexChoices.current = true;
|
||||
setChoiceOptions([...data.dynamic_query_function.complex_choices]);
|
||||
choicesInUse = [...data.dynamic_query_function.complex_choices];
|
||||
@@ -197,6 +211,7 @@ export function TaskParametersDialogRow(props){
|
||||
}
|
||||
}
|
||||
}catch(error){
|
||||
console.log(error);
|
||||
setBackdropOpen(false);
|
||||
snackActions.warning("Failed to parse dynamic parameter results");
|
||||
usingDynamicParamComplexChoices.current = false;
|
||||
@@ -269,7 +284,21 @@ export function TaskParametersDialogRow(props){
|
||||
console.log(data);
|
||||
}
|
||||
});
|
||||
const [deleteCredential] = useMutation(updateCredentialDeleted, {
|
||||
fetchPolicy: "no-cache",
|
||||
onCompleted: (data) => {
|
||||
snackActions.success("removed credential!");
|
||||
updateToLatestCredential.current = true;
|
||||
props.removedCredential(data.credential_by_pk);
|
||||
},
|
||||
onError: (data) => {
|
||||
snackActions.error("Failed to delete credential");
|
||||
console.log(data);
|
||||
}
|
||||
});
|
||||
const [treatNewlinesAsNewEntries, setTreatNewlinesAsNewEntries] = React.useState(false);
|
||||
const setAlarmAboutNoP2pRef = React.useRef(false);
|
||||
const locallySubmenuOpenPreventTaskingRef = React.useRef(false);
|
||||
const reIssueDynamicQueryFunction = () => {
|
||||
setBackdropOpen(true);
|
||||
snackActions.info("Querying payload type container for options...", {autoClose: 1000});
|
||||
@@ -283,7 +312,7 @@ export function TaskParametersDialogRow(props){
|
||||
usingDynamicParamChoices.current = true;
|
||||
}
|
||||
useEffect( () => {
|
||||
if(props.dynamic_query_function !== ""){
|
||||
if(props.dynamic_query_function !== ""){
|
||||
if(!usingDynamicParamChoices.current){
|
||||
setBackdropOpen(true);
|
||||
snackActions.info("Querying payload type container for options...", {autoClose: 1000});
|
||||
@@ -351,7 +380,7 @@ export function TaskParametersDialogRow(props){
|
||||
}
|
||||
}
|
||||
else if(props.type === "LinkInfo"){
|
||||
if(props.choices.length > 0){
|
||||
if(props.choices.length > 0 && value === ''){
|
||||
setChoiceOptions([...props.choices]);
|
||||
onChangeLinkInfo(0);
|
||||
}
|
||||
@@ -359,36 +388,69 @@ export function TaskParametersDialogRow(props){
|
||||
else if(props.type === "AgentConnect"){
|
||||
if(props.choices.length > 0){
|
||||
//setAgentConnectHost(0);
|
||||
let shouldUpdateMythicTrackingBecauseChoicesChanged = false;
|
||||
let hostNum = 0;
|
||||
if(agentConnectHost < props.choices.length){
|
||||
hostNum = agentConnectHost;
|
||||
}else{
|
||||
setAgentConnectHost(0);
|
||||
}
|
||||
setAgentConnectHostOptions(props.choices);
|
||||
if(arraysAreDifferent(props.choices, agentConnectHostOptions)){
|
||||
setAgentConnectHostOptions(props.choices);
|
||||
shouldUpdateMythicTrackingBecauseChoicesChanged = true;
|
||||
}
|
||||
let payloadNum = 0;
|
||||
if(agentConnectPayload < props.choices[hostNum]["payloads"].length){
|
||||
payloadNum = agentConnectPayload;
|
||||
}else{
|
||||
setAgentConnectPayload(0);
|
||||
}
|
||||
setAgentConnectPayloadOptions(props.choices[hostNum]["payloads"]);
|
||||
if(agentConnectPayload !== payloadNum){
|
||||
setAgentConnectPayload(payloadNum);
|
||||
}
|
||||
if(arraysAreDifferent(props.choices[hostNum]['payloads'], agentConnectPayloadOptions)){
|
||||
setAgentConnectPayloadOptions(props.choices[hostNum]["payloads"]);
|
||||
shouldUpdateMythicTrackingBecauseChoicesChanged = true;
|
||||
}
|
||||
if(props.choices[hostNum]["payloads"].length > 0){
|
||||
//setAgentConnectPayload(0);
|
||||
if(props.choices[hostNum]["payloads"][payloadNum]["c2info"].length > 0){
|
||||
setAgentConnectC2ProfileOptions(props.choices[hostNum]["payloads"][payloadNum]["c2info"]);
|
||||
if(arraysAreDifferent(props.choices[hostNum]["payloads"][payloadNum]["c2info"], agentConnectC2ProfileOptions)){
|
||||
setAgentConnectC2ProfileOptions(props.choices[hostNum]["payloads"][payloadNum]["c2info"]);
|
||||
shouldUpdateMythicTrackingBecauseChoicesChanged = true;
|
||||
}
|
||||
//setAgentConnectC2Profile(0);
|
||||
if(agentConnectHost !== hostNum || agentConnectPayload !== payloadNum || shouldUpdateMythicTrackingBecauseChoicesChanged){
|
||||
onChangeAgentConnect(hostNum, payloadNum, 0);
|
||||
}
|
||||
}
|
||||
if(locallySubmenuOpenPreventTaskingRef.current){
|
||||
props.setSubmenuOpenPreventTasking(false);
|
||||
locallySubmenuOpenPreventTaskingRef.current = false;
|
||||
}
|
||||
}else{
|
||||
snackActions.warning("Mythic knows of no host with a P2P payload. Please add one.");
|
||||
props.setSubmenuOpenPreventTasking(true);
|
||||
if(!setAlarmAboutNoP2pRef.current){
|
||||
setAlarmAboutNoP2pRef.current = true;
|
||||
snackActions.warning("Mythic knows of no host with a P2P payload. Please add one.");
|
||||
props.setSubmenuOpenPreventTasking(true);
|
||||
locallySubmenuOpenPreventTaskingRef.current = true;
|
||||
}
|
||||
|
||||
}
|
||||
}else{
|
||||
setAgentConnectHostOptions([]);
|
||||
setAgentConnectPayloadOptions([]);
|
||||
setAgentConnectC2ProfileOptions([]);
|
||||
snackActions.warning("Mythic knows of no host with a P2P payload. Please add one.");
|
||||
props.setSubmenuOpenPreventTasking(true);
|
||||
if(agentConnectHostOptions.length > 0){
|
||||
setAgentConnectHostOptions([]);
|
||||
}
|
||||
if(agentConnectPayloadOptions.length > 0){
|
||||
setAgentConnectPayloadOptions([]);
|
||||
}
|
||||
if(agentConnectC2ProfileOptions.length > 0){
|
||||
setAgentConnectC2ProfileOptions([]);
|
||||
}
|
||||
if(!setAlarmAboutNoP2pRef.current){
|
||||
setAlarmAboutNoP2pRef.current = true;
|
||||
snackActions.warning("Mythic knows of no host with a P2P payload. Please add one.");
|
||||
props.setSubmenuOpenPreventTasking(true);
|
||||
locallySubmenuOpenPreventTaskingRef.current = true;
|
||||
}
|
||||
}
|
||||
}else{
|
||||
if(value === ""){
|
||||
@@ -411,8 +473,8 @@ export function TaskParametersDialogRow(props){
|
||||
//console.log("updating choiceOptions from useEffect in dialog row: ", [...props.choices])
|
||||
setChoiceOptions([...props.choices]);
|
||||
if(updateToLatestCredential.current){
|
||||
setValue(props.choices.length-1);
|
||||
props.onChange(props.name, {...props.choices[props.choices.length-1]}, false);
|
||||
setValue(0);
|
||||
props.onChange(props.name, {...props.choices[0]}, false);
|
||||
updateToLatestCredential.current = false;
|
||||
}
|
||||
if(value === ""){
|
||||
@@ -428,7 +490,9 @@ export function TaskParametersDialogRow(props){
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [props.choices, props.default_value, props.type, props.value, setBoolValue, value]);
|
||||
}, [props.choices, props.default_value, props.type, props.value, setBoolValue,
|
||||
value, agentConnectHost, agentConnectPayload, agentConnectPayloadOptions, agentConnectHostOptions,
|
||||
agentConnectC2ProfileOptions]);
|
||||
const onChangeAgentConnect = (host_index, payload_index, c2_index) => {
|
||||
const c2profileparameters = props.choices[host_index]["payloads"][payload_index]["c2info"][c2_index].parameters.reduce( (prev, opt) => {
|
||||
return {...prev, [opt.name]: opt.value}
|
||||
@@ -496,13 +560,13 @@ export function TaskParametersDialogRow(props){
|
||||
setValue(event.target.checked);
|
||||
props.onChange(props.name, event.target.checked);
|
||||
}
|
||||
const onFileChange = (evt) => {
|
||||
setFileValue({name: evt.target.files[0].name});
|
||||
props.onChange(props.name, evt.target.files[0]);
|
||||
const onFileChange = (newFile) => {
|
||||
setFileValue({name: newFile.name});
|
||||
props.onChange(props.name, newFile);
|
||||
}
|
||||
const onFileMultChange = (evt) => {
|
||||
setFileMultValue([...evt.target.files]);
|
||||
props.onChange(props.name, [...evt.target.files]);
|
||||
const onFileMultChange = (newFiles) => {
|
||||
setFileMultValue([...newFiles]);
|
||||
props.onChange(props.name, [...newFiles]);
|
||||
}
|
||||
const onChangeAgentConnectHost = (event) => {
|
||||
setAgentConnectHost(event.target.value);
|
||||
@@ -550,6 +614,7 @@ export function TaskParametersDialogRow(props){
|
||||
return;
|
||||
}
|
||||
props.setSubmenuOpenPreventTasking(false);
|
||||
locallySubmenuOpenPreventTaskingRef.current = false;
|
||||
props.onAgentConnectAddNewPayloadOnHost(agentConnectNewHost.toUpperCase(), props.payload_choices[agentConnectNewPayload].id);
|
||||
setOpenAdditionalPayloadOnHostmenu(false);
|
||||
}
|
||||
@@ -645,6 +710,9 @@ export function TaskParametersDialogRow(props){
|
||||
const onCreateCredential = ({type, account, realm, comment, credential}) => {
|
||||
createCredential({variables: {type, account, realm, comment, credential}})
|
||||
}
|
||||
const onDeleteCredential = () => {
|
||||
deleteCredential({variables: {deleted: true, credential_id: ChoiceOptions[value].id}})
|
||||
}
|
||||
const getParameterObject = () => {
|
||||
switch(props.type){
|
||||
case "ChooseOneCustom":
|
||||
@@ -676,6 +744,7 @@ export function TaskParametersDialogRow(props){
|
||||
</FormControl>
|
||||
OR
|
||||
<MythicTextField required={props.required} placeholder={"Custom Value"} value={chooseOneCustomValue} multiline={true} maxRows={5}
|
||||
validate={testParameterValues} errorText={"Must match: " + props.verifier_regex}
|
||||
onChange={onChangeTextChooseOneCustom} display="inline-block" onEnter={props.onSubmit} autoFocus={props.autoFocus}
|
||||
name={props.name} marginTop={"5px"}
|
||||
/>
|
||||
@@ -855,28 +924,14 @@ export function TaskParametersDialogRow(props){
|
||||
case "File":
|
||||
return (
|
||||
<>
|
||||
<Button variant="contained" component="label">
|
||||
{ fileValue.name === "" ? "Select File" : fileValue.name }
|
||||
<input onChange={onFileChange} type="file" hidden />
|
||||
</Button>
|
||||
<DragAndDropFileUpload value={fileValue} multiple={false} onChange={onFileChange} />
|
||||
</>
|
||||
|
||||
)
|
||||
case "FileMultiple":
|
||||
return (
|
||||
<>
|
||||
<Button variant="contained" component="label">
|
||||
Select Files
|
||||
<input onChange={onFileMultChange} type="file" hidden multiple />
|
||||
</Button>
|
||||
{ fileMultValue.length > 0 &&
|
||||
fileMultValue.map((f, i) => (
|
||||
<div key={i}>
|
||||
{typeof f === "string" && <MythicFileContext agent_file_id={f} />}
|
||||
{typeof f !== "string" && (f.name)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
<DragAndDropFileUpload values={fileMultValue} multiple={true} onChange={onFileMultChange} />
|
||||
</>
|
||||
)
|
||||
case "LinkInfo":
|
||||
@@ -940,7 +995,7 @@ export function TaskParametersDialogRow(props){
|
||||
<MythicStyledTableCell>
|
||||
<FormControl style={{width: "100%"}}>
|
||||
<Select
|
||||
value={agentConnectNewPayload}
|
||||
value={props.payload_choices.length > 0 ? agentConnectNewPayload : ''}
|
||||
onChange={onChangeAgentConnectNewPayload}
|
||||
input={<Input />}
|
||||
>
|
||||
@@ -965,6 +1020,7 @@ export function TaskParametersDialogRow(props){
|
||||
<Button component="span" style={{color: theme.palette.warning.main, padding: 0}} onClick={() =>{
|
||||
setOpenAdditionalPayloadOnHostmenu(false);
|
||||
props.setSubmenuOpenPreventTasking(false);
|
||||
locallySubmenuOpenPreventTaskingRef.current = false;
|
||||
}}><CancelIcon />Cancel</Button>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
@@ -977,7 +1033,7 @@ export function TaskParametersDialogRow(props){
|
||||
<MythicStyledTableCell>
|
||||
<FormControl style={{width: "100%"}}>
|
||||
<Select
|
||||
value={agentConnectHost}
|
||||
value={agentConnectHostOptions.length > 0 ? agentConnectHost : ''}
|
||||
onChange={onChangeAgentConnectHost}
|
||||
input={<Input />}
|
||||
>
|
||||
@@ -991,11 +1047,11 @@ export function TaskParametersDialogRow(props){
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell>Payload</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>Payload / Callback</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<FormControl style={{width: "100%"}}>
|
||||
<Select
|
||||
value={agentConnectPayload}
|
||||
value={agentConnectPayloadOptions.length > 0 ? agentConnectPayload : ''}
|
||||
onChange={onChangeAgentConnectPayload}
|
||||
input={<Input />}
|
||||
>
|
||||
@@ -1019,6 +1075,7 @@ export function TaskParametersDialogRow(props){
|
||||
<Button component="span" style={{color: theme.palette.success.main, padding: 0}} onClick={() =>{
|
||||
setOpenAdditionalPayloadOnHostmenu(true);
|
||||
props.setSubmenuOpenPreventTasking(true);
|
||||
locallySubmenuOpenPreventTaskingRef.current = true;
|
||||
}}><AddCircleIcon />Register New</Button>
|
||||
</MythicStyledTooltip>
|
||||
|
||||
@@ -1035,7 +1092,7 @@ export function TaskParametersDialogRow(props){
|
||||
<MythicStyledTableCell>
|
||||
<FormControl style={{width: "100%"}}>
|
||||
<Select
|
||||
value={agentConnectC2Profile}
|
||||
value={agentConnectC2ProfileOptions.length > 0 ? agentConnectC2Profile : ''}
|
||||
onChange={onChangeAgentConnectC2Profile}
|
||||
input={<Input />}
|
||||
>
|
||||
@@ -1074,10 +1131,12 @@ export function TaskParametersDialogRow(props){
|
||||
case "CredentialJson":
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={createCredentialDialogOpen}
|
||||
onClose={()=>{setCreateCredentialDialogOpen(false);}}
|
||||
innerDialog={<CredentialTableNewCredentialDialog onSubmit={onCreateCredential} onClose={()=>{setCreateCredentialDialogOpen(false);}} />}
|
||||
/>
|
||||
{createCredentialDialogOpen &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={createCredentialDialogOpen}
|
||||
onClose={()=>{setCreateCredentialDialogOpen(false);}}
|
||||
innerDialog={<CredentialTableNewCredentialDialog onSubmit={onCreateCredential} onClose={()=>{setCreateCredentialDialogOpen(false);}} />}
|
||||
/>
|
||||
}
|
||||
<FormControl style={{width: "100%"}}>
|
||||
<Select
|
||||
value={value}
|
||||
@@ -1090,8 +1149,14 @@ export function TaskParametersDialogRow(props){
|
||||
<MenuItem key={props.name + i} value={i}>
|
||||
<Typography style={{wordBreak: "break-all", whiteSpace: "pre-wrap"}}>
|
||||
{opt.account + (opt.realm === "" ? "" : "@" + opt.realm) + " - " +
|
||||
(opt.credential_text.length > 40 ? opt.credential_text.substring(0, 40) + "..." : opt.credential_text) +
|
||||
(opt.comment.length > 0 ? "\n" + opt.comment : "")}
|
||||
(opt.credential_text.length > 40 ? opt.credential_text.substring(0, 40) + "..." : opt.credential_text)}
|
||||
{opt.comment.length > 0 ?
|
||||
(
|
||||
<>
|
||||
<b>{"\nComment: "}</b> {opt.comment}
|
||||
</>
|
||||
)
|
||||
: ""}
|
||||
</Typography>
|
||||
|
||||
</MenuItem>
|
||||
@@ -1099,7 +1164,20 @@ export function TaskParametersDialogRow(props){
|
||||
}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Button size="small" color="primary" onClick={ () => {setCreateCredentialDialogOpen(true);}} variant="contained">New Credential</Button>
|
||||
<Button color={"success"} component="span" style={{marginRight: "20px"}} onClick={() =>{
|
||||
setCreateCredentialDialogOpen(true)
|
||||
}}><AddCircleIcon style={{
|
||||
color: theme.palette.success.main,
|
||||
background: "white",
|
||||
borderRadius: "10px",
|
||||
marginRight: "5px"
|
||||
}}/> Credential</Button>
|
||||
<Button color={"warning"} component="span" style={{padding: 0}} onClick={() =>{
|
||||
onDeleteCredential();
|
||||
}}><DeleteIcon style={{
|
||||
color: theme.palette.error.main,
|
||||
marginRight: "5px"
|
||||
}}/> Credential</Button>
|
||||
</React.Fragment>
|
||||
|
||||
)
|
||||
@@ -1110,12 +1188,16 @@ export function TaskParametersDialogRow(props){
|
||||
return (
|
||||
<TableRow key={"buildparam" + props.id}>
|
||||
<MythicStyledTableCell >
|
||||
<MythicStyledTooltip title={props.description.length > 0 ? props.description : "No Description"}>
|
||||
<Typography style={{fontWeight: "600", wordBreak: "break-all"}} >
|
||||
{props.display_name}
|
||||
</MythicStyledTooltip>
|
||||
{props.required ? (
|
||||
<Typography component="div" style={{color: theme.palette.warning.main}}>Required</Typography>
|
||||
) : null }
|
||||
</Typography>
|
||||
<Typography variant={"body2"} style={{
|
||||
fontSize: theme.typography.pxToRem(14),
|
||||
wordBreak: "break-all"
|
||||
}}>
|
||||
{props.description}
|
||||
</Typography>
|
||||
{props.required && <Typography component="div" color={"warning"}>Required</Typography>}
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
{getParameterObject()}
|
||||
@@ -1124,3 +1206,115 @@ export function TaskParametersDialogRow(props){
|
||||
)
|
||||
}
|
||||
|
||||
export const DragAndDropFileUpload = ({value, values, multiple, onChange}) => {
|
||||
const theme = useTheme();
|
||||
const inputRef = React.useRef(null);
|
||||
const [files, setFiles] = React.useState(values ? values : []);
|
||||
const [file, setFile] = React.useState(value ? value : {name: ""});
|
||||
const [isDragging, setIsDragging] = React.useState(false);
|
||||
|
||||
React.useEffect( () => {
|
||||
if(value){
|
||||
setFile(value);
|
||||
}
|
||||
if(values){
|
||||
setFiles(values);
|
||||
}
|
||||
}, [value, values]);
|
||||
const handleDragEnter = () => setIsDragging(true);
|
||||
const handleDragLeave = (event) => {
|
||||
if (event.currentTarget.contains(event.relatedTarget)) {
|
||||
return; // Don't treat as a real leave event
|
||||
}
|
||||
setIsDragging(false);
|
||||
}
|
||||
const handleDrop = (e) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
const droppedFiles = Array.from(e.dataTransfer.files);
|
||||
if(multiple){
|
||||
setFiles(droppedFiles);
|
||||
onChange(droppedFiles)
|
||||
} else {
|
||||
setFile(droppedFiles[0]);
|
||||
onChange(droppedFiles[0]);
|
||||
}
|
||||
};
|
||||
const handleDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
const onFileChange = (evt) => {
|
||||
setFile({name: evt.target.files[0].name});
|
||||
onChange(evt.target.files[0]);
|
||||
}
|
||||
const onFileMultChange = (evt) => {
|
||||
setFiles([...evt.target.files]);
|
||||
onChange(evt.target.files)
|
||||
}
|
||||
const onClick = (e) => {
|
||||
inputRef.current.click();
|
||||
}
|
||||
return (
|
||||
<div
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onClick={onClick}
|
||||
style={{
|
||||
border: isDragging ? `4px dashed ${theme.palette.success.main}` : `4px dashed ${theme.palette.primary.main}`,
|
||||
padding: "20px",
|
||||
textAlign: "center",
|
||||
borderRadius: "10px",
|
||||
cursor: "pointer"
|
||||
//backgroundColor: isDragging ? "#f0f8ff" : "#fff",
|
||||
}}
|
||||
>
|
||||
<input ref={inputRef} onChange={multiple ? onFileMultChange : onFileChange} type="file" hidden multiple={multiple} />
|
||||
<div style={{display: "flex", flexDirection: "column", alignItems: "center"}}>
|
||||
{!multiple && file.name !== "" &&
|
||||
<>
|
||||
<CloudUploadTwoToneIcon fontSize={"large"} color={"success"} />
|
||||
<Typography>
|
||||
Selected:
|
||||
</Typography>
|
||||
<Typography>
|
||||
{!file.legacy && file.name}
|
||||
{file.legacy &&
|
||||
<MythicFileContext agent_file_id={file.name}
|
||||
extraStyles={{ position: "relative", marginLeft: "5px", marginRight: "5px"}} />
|
||||
}
|
||||
</Typography>
|
||||
</>
|
||||
|
||||
}
|
||||
{multiple && files.length > 0 &&
|
||||
<>
|
||||
<CloudUploadTwoToneIcon fontSize={"large"} color={"success"} />
|
||||
<Typography>
|
||||
Selected:
|
||||
</Typography>
|
||||
{files.map( (f, i) => (
|
||||
<div key={i}>
|
||||
{typeof f === "string" && <MythicFileContext agent_file_id={f} />}
|
||||
{typeof f !== "string" && (f.name)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
|
||||
}
|
||||
{file.name === "" && files.length === 0 &&
|
||||
<>
|
||||
<CloudUploadTwoToneIcon fontSize={"large"} color={"success"} />
|
||||
<Typography>
|
||||
Drag and drop files here
|
||||
</Typography>
|
||||
<Typography>
|
||||
Click to open dialog
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {MythicDraggableDialogTitle} from "../../MythicComponents/MythicDraggableDialogTitle";
|
||||
|
||||
export const allTokenDataFragment = gql`
|
||||
fragment allTokenData on token {
|
||||
@@ -89,7 +89,7 @@ export function TaskTokenDialog(props) {
|
||||
});
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Token Information</DialogTitle>
|
||||
<MythicDraggableDialogTitle >Token Information</MythicDraggableDialogTitle>
|
||||
<TableContainer className="mythicElement">
|
||||
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
|
||||
<TableHead>
|
||||
|
||||
@@ -10,6 +10,7 @@ query getParametersQuery ($task_id: Int!) {
|
||||
task_by_pk(id: $task_id) {
|
||||
display_params
|
||||
original_params
|
||||
mythic_parsed_params
|
||||
params
|
||||
tasking_location
|
||||
parameter_group_name
|
||||
@@ -37,6 +38,8 @@ export function TaskViewParametersDialog(props) {
|
||||
variables: {task_id: props.task_id},
|
||||
onCompleted: data => {
|
||||
let workingComment = "Original Parameters:\n" + data.task_by_pk.original_params;
|
||||
workingComment += "\n\nMythic Parsed Parameters: \n\tUse this for eventing (create_task->action_data->params_dictionary) and scripting (mythic.issue_task->parameters)\n"
|
||||
workingComment += data.task_by_pk.mythic_parsed_params;
|
||||
workingComment += "\n\nAgent Parameters:\n" + data.task_by_pk.params;
|
||||
workingComment += "\n\nDisplay Parameters:\n" + data.task_by_pk.display_params;
|
||||
workingComment += "\n\nTasking Location:\n" + data.task_by_pk.tasking_location;
|
||||
@@ -65,13 +68,14 @@ export function TaskViewParametersDialog(props) {
|
||||
return <div>Error!</div>;
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicModifyStringDialog title={`View Task Parameters And Timestamps`}
|
||||
onClose={props.onClose}
|
||||
maxRows={40}
|
||||
wrap={true}
|
||||
value={comment} />
|
||||
</React.Fragment>
|
||||
<>
|
||||
<MythicModifyStringDialog title={`View Task Parameters And Timestamps`}
|
||||
onClose={props.onClose}
|
||||
maxRows={40}
|
||||
wrap={true}
|
||||
value={comment} />
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ query getParametersQuery ($task_id: Int!) {
|
||||
|
||||
export function TaskViewStdoutStderrDialog(props) {
|
||||
const [comment, setComment] = useState("");
|
||||
const theme = useTheme();
|
||||
const { loading, error } = useQuery(getParametersQuery, {
|
||||
variables: {task_id: props.task_id},
|
||||
onCompleted: data => {
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import React from 'react';
|
||||
import { ConsumingServicesTable } from './ConsumingServicesTable';
|
||||
import {useSubscription, gql } from '@apollo/client';
|
||||
const sub_consuming_services = gql`
|
||||
subscription ConsumingContainer{
|
||||
consuming_container_stream(batch_size: 10, cursor: {initial_value: {updated_at: "1970-01-01"}}) {
|
||||
id
|
||||
name
|
||||
description
|
||||
type
|
||||
container_running
|
||||
deleted
|
||||
subscriptions
|
||||
}
|
||||
}
|
||||
`;
|
||||
export function ConsumingServices({me}){
|
||||
const [services, setServices] = React.useState([]);
|
||||
useSubscription(sub_consuming_services, {
|
||||
fetchPolicy: "no-cache",
|
||||
onData: ({data}) => {
|
||||
const updated = data.data.consuming_container_stream.reduce( (prev, cur) => {
|
||||
let existingIndex = prev.findIndex( (element, i, array) => element.id === cur.id);
|
||||
if(existingIndex > -1){
|
||||
prev[existingIndex] = {...prev[existingIndex], ...cur};
|
||||
return [...prev];
|
||||
}
|
||||
return [...prev, cur];
|
||||
}, services);
|
||||
setServices(updated);
|
||||
},
|
||||
onError: ({data}) => {
|
||||
console.log(data)
|
||||
},
|
||||
});
|
||||
return (
|
||||
<div style={{display: "flex", flexDirection: "column", height: "100%"}}>
|
||||
<ConsumingServicesTable
|
||||
me={me}
|
||||
services={services}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,920 +0,0 @@
|
||||
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 {useTheme} from '@mui/material/styles';
|
||||
import PublicIcon from '@mui/icons-material/Public';
|
||||
import {IconButton} from '@mui/material';
|
||||
import {gql, useMutation} from '@apollo/client';
|
||||
import {snackActions} from '../../utilities/Snackbar';
|
||||
import SyncAltIcon from '@mui/icons-material/SyncAlt';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import MythicTableCell from "../../MythicComponents/MythicTableCell";
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import RestoreFromTrashOutlinedIcon from '@mui/icons-material/RestoreFromTrashOutlined';
|
||||
import {MythicConfirmDialog} from "../../MythicComponents/MythicConfirmDialog";
|
||||
import PermIdentityTwoToneIcon from '@mui/icons-material/PermIdentityTwoTone';
|
||||
import {MythicDialog} from "../../MythicComponents/MythicDialog";
|
||||
import AttachFileIcon from '@mui/icons-material/AttachFile';
|
||||
import {ConsumingServicesGetIDPMetadataDialog} from "./ConsumingServicesGetIDPMetadataDialog";
|
||||
import {C2ProfileListFilesDialog} from "../PayloadTypesC2Profiles/C2ProfileListFilesDialog";
|
||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
|
||||
const testWebhookMutation = gql`
|
||||
mutation testWebhookWorks($service_type: String!){
|
||||
consumingServicesTestWebhook(service_type: $service_type){
|
||||
status
|
||||
error
|
||||
}
|
||||
}
|
||||
`;
|
||||
const testLogMutation = gql`
|
||||
mutation testWebhookWorks($service_type: String!){
|
||||
consumingServicesTestLog(service_type: $service_type){
|
||||
status
|
||||
error
|
||||
}
|
||||
}
|
||||
`;
|
||||
const toggleDeleteStatus = gql`
|
||||
mutation toggleConsumingContainerDeleteStatus($id: Int!, $deleted: Boolean!){
|
||||
update_consuming_container_by_pk(pk_columns: {id: $id}, _set: {deleted: $deleted}) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
const webhook_events = ["new_alert","new_callback","new_custom","new_feedback", "new_startup"];
|
||||
const logging_events = ["new_artifact","new_callback", "new_credential","new_file", "new_keylog", "new_payload", "new_response", "new_task"];
|
||||
|
||||
export function ConsumingServicesTable({services}) {
|
||||
const theme = useTheme();
|
||||
const [showDeleted, setShowDeleted] = React.useState(false);
|
||||
const [testWebhook] = useMutation(testWebhookMutation, {
|
||||
onCompleted: data => {
|
||||
if (data.consumingServicesTestWebhook.status === "success") {
|
||||
snackActions.success("Successfully sent test message to service");
|
||||
} else {
|
||||
console.log(data.consumingServicesTestWebhook.error)
|
||||
snackActions.error("No webhook listening")
|
||||
}
|
||||
|
||||
},
|
||||
onError: error => {
|
||||
|
||||
}
|
||||
});
|
||||
const issueTestWebhook = (service_type) => {
|
||||
testWebhook({variables: {service_type: service_type}});
|
||||
}
|
||||
const [testLog] = useMutation(testLogMutation, {
|
||||
onCompleted: data => {
|
||||
if (data.consumingServicesTestLog.status === "success") {
|
||||
snackActions.success("Successfully sent test message to service");
|
||||
} else {
|
||||
snackActions.error("No logger listening")
|
||||
console.log(data.consumingServicesTestLog.error)
|
||||
}
|
||||
|
||||
},
|
||||
onError: error => {
|
||||
|
||||
}
|
||||
});
|
||||
const issueTestLog = (service_type) => {
|
||||
testLog({variables: {service_type: service_type}});
|
||||
}
|
||||
const [openIDPMetadata, setOpenIDPMetadata] = React.useState(false);
|
||||
const IDPMetadataRef = React.useRef({"container": "", "idp": ""});
|
||||
const openListFilesInformation = React.useRef("");
|
||||
const [webhooks, setWebhooks] = React.useState([]);
|
||||
const [logging, setLogging] = React.useState([]);
|
||||
const [eventing, setEventing] = React.useState([]);
|
||||
const [auth, setAuth] = React.useState([]);
|
||||
const [openListFilesDialog, setOpenListFilesDialog] = React.useState(false);
|
||||
const [openDelete, setOpenDeleteDialog] = React.useState(false);
|
||||
const deletingContainer = React.useRef({});
|
||||
const [updateDeleted] = useMutation(toggleDeleteStatus, {
|
||||
onCompleted: data => {
|
||||
snackActions.success("Successfully updated");
|
||||
},
|
||||
onError: error => {
|
||||
console.log(error);
|
||||
snackActions.error("Failed to update: " + error.message);
|
||||
}
|
||||
});
|
||||
const onAcceptDelete = () => {
|
||||
updateDeleted({variables: {id: deletingContainer.current.id, deleted: !deletingContainer.current.deleted}})
|
||||
setOpenDeleteDialog(false);
|
||||
}
|
||||
const adjustingDelete = (service) => {
|
||||
deletingContainer.current = service;
|
||||
setOpenDeleteDialog(true);
|
||||
}
|
||||
const getIDPMetadata = (container, idp) => {
|
||||
IDPMetadataRef.current = {"container": container, "idp": idp};
|
||||
setOpenIDPMetadata(true);
|
||||
}
|
||||
const onOpenListFilesDialog = (container) => {
|
||||
openListFilesInformation.current = container;
|
||||
setOpenListFilesDialog(true);
|
||||
}
|
||||
React.useEffect(() => {
|
||||
const localWebhooks = services.filter(cur => cur.type === "webhook").sort((a, b) => a.name < b.name ? 1 : -1);
|
||||
setWebhooks(localWebhooks);
|
||||
const localLogging = services.filter(cur => cur.type === "logging").sort((a, b) => a.name < b.name ? 1 : -1);
|
||||
setLogging(localLogging);
|
||||
const localEventing = services.filter(cur => cur.type === "eventing").sort((a, b) => a.name < b.name ? 1 : -1);
|
||||
const parsedLocalEventing = localEventing.map( e => {
|
||||
const newSubs = e.subscriptions.map( s => {
|
||||
try{
|
||||
return JSON.parse(s);
|
||||
}catch(error){
|
||||
console.log(error);
|
||||
return {name: "", description: s};
|
||||
}
|
||||
});
|
||||
return {...e, subscriptions: newSubs}
|
||||
})
|
||||
setEventing(parsedLocalEventing);
|
||||
const localAuth = services.filter(cur => cur.type === "auth").sort((a, b) => a.name < b.name ? 1 : -1);
|
||||
const parsedLocalAuth = localAuth.map( e => {
|
||||
const newSubs = e.subscriptions.map( s => {
|
||||
try{
|
||||
return JSON.parse(s);
|
||||
}catch(error){
|
||||
console.log(error);
|
||||
return {name: s, type: ""};
|
||||
}
|
||||
});
|
||||
return {...e, subscriptions: newSubs}
|
||||
})
|
||||
setAuth(parsedLocalAuth);
|
||||
}, [services]);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Paper elevation={5} style={{
|
||||
backgroundColor: theme.pageHeader.main,
|
||||
color: theme.pageHeaderText.main,
|
||||
marginBottom: "5px",
|
||||
marginRight: "5px"
|
||||
}} variant={"elevation"}>
|
||||
<Typography variant="h5" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
|
||||
Containers Consuming Events
|
||||
</Typography>
|
||||
{showDeleted ? (
|
||||
<MythicStyledTooltip title={"Hide Deleted Services"} tooltipStyle={{float: "right"}}>
|
||||
<IconButton size="small" variant="contained" onClick={() => setShowDeleted(!showDeleted)}><VisibilityIcon /></IconButton>
|
||||
</MythicStyledTooltip>
|
||||
|
||||
) : (
|
||||
<MythicStyledTooltip title={"Show Deleted Services"} tooltipStyle={{float: "right"}}>
|
||||
<IconButton size="small" variant="contained" onClick={() => setShowDeleted(!showDeleted)} ><VisibilityOffIcon /></IconButton>
|
||||
</MythicStyledTooltip>
|
||||
)}
|
||||
</Paper>
|
||||
<div style={{display: "flex", flexGrow: 1}}>
|
||||
<TableContainer className="mythicElement">
|
||||
<Table stickyHeader={true} size="small" style={{"tableLayout": "fixed", "overflow": "scroll"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{width: "3rem"}}></TableCell>
|
||||
<TableCell style={{width: "30%"}}>Name</TableCell>
|
||||
<TableCell style={{width: "5rem"}}>Manage</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{webhooks.map( (w, index) => (
|
||||
(showDeleted || !w.deleted) &&
|
||||
<TableRow key={w.id} hover>
|
||||
<MythicTableCell>
|
||||
{w.deleted ? (
|
||||
<IconButton onClick={() => {
|
||||
adjustingDelete(w);
|
||||
}} color="success" size="medium">
|
||||
<RestoreFromTrashOutlinedIcon/>
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton onClick={() => {
|
||||
adjustingDelete(w);
|
||||
}} color="error" size="medium">
|
||||
<DeleteIcon/>
|
||||
</IconButton>
|
||||
)}
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<Typography variant={"h5"}>
|
||||
{w.name}
|
||||
</Typography>
|
||||
<Typography variant={"body"}>
|
||||
<b>Type: </b>{w.type}
|
||||
</Typography>
|
||||
<Typography variant={"body2"}>
|
||||
<b>Description: </b>{w.description}
|
||||
</Typography>
|
||||
|
||||
{w.container_running &&
|
||||
<Typography variant="body2" component="p"
|
||||
style={{color: theme.palette.success.main}}>
|
||||
<b>{"Online"}</b>
|
||||
</Typography>
|
||||
}
|
||||
{!w.container_running &&
|
||||
<Typography variant="body2" component="p"
|
||||
style={{color: theme.palette.error.main}}>
|
||||
<b>{"Offline"}</b>
|
||||
</Typography>
|
||||
}
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
|
||||
<IconButton
|
||||
color={"secondary"}
|
||||
disabled={!w.container_running}
|
||||
onClick={()=>{onOpenListFilesDialog(w.name);}}
|
||||
size="medium">
|
||||
<AttachFileIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
{webhook_events.map(s => (
|
||||
<MythicStyledTooltip title={"test webhook " + s} key={w.id + "webhook_" + s}>
|
||||
<IconButton
|
||||
disabled={!w.subscriptions.includes(s)}
|
||||
onClick={() => {
|
||||
issueTestWebhook(s)
|
||||
}}
|
||||
size="medium">
|
||||
<PublicIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
))}
|
||||
</MythicTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{logging.map(w => (
|
||||
(showDeleted || !w.deleted) &&
|
||||
<TableRow key={w.id} hover>
|
||||
<MythicTableCell>
|
||||
{w.deleted ? (
|
||||
<IconButton onClick={() => {
|
||||
adjustingDelete(w);
|
||||
}} color="success" size="medium">
|
||||
<RestoreFromTrashOutlinedIcon/>
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton onClick={() => {
|
||||
adjustingDelete(w);
|
||||
}} color="error" size="medium">
|
||||
<DeleteIcon/>
|
||||
</IconButton>
|
||||
)}
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<Typography variant={"h5"}>
|
||||
{w.name}
|
||||
</Typography>
|
||||
<Typography variant={"body"}>
|
||||
<b>Type: </b>{w.type}
|
||||
</Typography>
|
||||
<Typography variant={"body2"}>
|
||||
<b>Description: </b>{w.description}
|
||||
</Typography>
|
||||
{w.container_running &&
|
||||
<Typography variant="body2" component="p"
|
||||
style={{color: theme.palette.success.main}}>
|
||||
<b>{"Online"}</b>
|
||||
</Typography>
|
||||
}
|
||||
{!w.container_running &&
|
||||
<Typography variant="body2" component="p"
|
||||
style={{color: theme.palette.error.main}}>
|
||||
<b>{"Offline"}</b>
|
||||
</Typography>
|
||||
}
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
|
||||
<IconButton
|
||||
color={"secondary"}
|
||||
disabled={!w.container_running}
|
||||
onClick={()=>{onOpenListFilesDialog(w.name);}}
|
||||
size="medium">
|
||||
<AttachFileIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
{logging_events.map(s => (
|
||||
<MythicStyledTooltip title={"test logging " + s} key={w.id + "logging_" + s}>
|
||||
<IconButton
|
||||
disabled={!w.subscriptions.includes(s)}
|
||||
onClick={() => {
|
||||
issueTestLog(s)
|
||||
}}
|
||||
size="medium">
|
||||
<SyncAltIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
))}
|
||||
</MythicTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{eventing.map( (w, index) => (
|
||||
(showDeleted || !w.deleted) &&
|
||||
<TableRow key={w.id} hover>
|
||||
<MythicTableCell>
|
||||
{w.deleted ? (
|
||||
<IconButton onClick={() => {
|
||||
adjustingDelete(w);
|
||||
}} color="success" size="medium">
|
||||
<RestoreFromTrashOutlinedIcon/>
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton onClick={() => {
|
||||
adjustingDelete(w);
|
||||
}} color="error" size="medium">
|
||||
<DeleteIcon/>
|
||||
</IconButton>
|
||||
)}
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<Typography variant={"h5"}>
|
||||
{w.name}
|
||||
</Typography>
|
||||
<Typography variant={"body"}>
|
||||
<b>Type: </b>{w.type}
|
||||
</Typography>
|
||||
<Typography variant={"body2"}>
|
||||
<b>Description: </b>{w.description}
|
||||
</Typography>
|
||||
{w.container_running &&
|
||||
<Typography variant="body2" component="p"
|
||||
style={{color: theme.palette.success.main}}>
|
||||
<b>{"Online"}</b>
|
||||
</Typography>
|
||||
}
|
||||
{!w.container_running &&
|
||||
<Typography variant="body2" component="p"
|
||||
style={{color: theme.palette.error.main}}>
|
||||
<b>{"Offline"}</b>
|
||||
</Typography>
|
||||
}
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
|
||||
<IconButton
|
||||
color={"secondary"}
|
||||
disabled={!w.container_running}
|
||||
onClick={()=>{onOpenListFilesDialog(w.name);}}
|
||||
size="medium">
|
||||
<AttachFileIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Function</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{w.subscriptions.map(s => (
|
||||
<TableRow key={s.name} hover>
|
||||
<MythicTableCell><b>{s.name}</b></MythicTableCell>
|
||||
<MythicTableCell>{s.description}</MythicTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
</MythicTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{auth.map( (w, index) => (
|
||||
(showDeleted || !w.deleted) &&
|
||||
<TableRow key={w.id} hover>
|
||||
<MythicTableCell>
|
||||
{w.deleted ? (
|
||||
<IconButton onClick={() => {
|
||||
adjustingDelete(w);
|
||||
}} color="success" size="medium">
|
||||
<RestoreFromTrashOutlinedIcon/>
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton onClick={() => {
|
||||
adjustingDelete(w);
|
||||
}} color="error" size="medium">
|
||||
<DeleteIcon/>
|
||||
</IconButton>
|
||||
)}
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<Typography variant={"h5"}>
|
||||
{w.name}
|
||||
</Typography>
|
||||
<Typography variant={"body"}>
|
||||
<b>Type: </b>{w.type}
|
||||
</Typography>
|
||||
<Typography variant={"body2"}>
|
||||
<b>Description: </b>{w.description}
|
||||
</Typography>
|
||||
{w.container_running &&
|
||||
<Typography variant="body2" component="p"
|
||||
style={{color: theme.palette.success.main}}>
|
||||
<b>{"Online"}</b>
|
||||
</Typography>
|
||||
}
|
||||
{!w.container_running &&
|
||||
<Typography variant="body2" component="p"
|
||||
style={{color: theme.palette.error.main}}>
|
||||
<b>{"Offline"}</b>
|
||||
</Typography>
|
||||
}
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
|
||||
<IconButton
|
||||
color={"secondary"}
|
||||
disabled={!w.container_running}
|
||||
onClick={()=>{onOpenListFilesDialog(w.name);}}
|
||||
size="medium">
|
||||
<AttachFileIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
{w.subscriptions.map(s => (
|
||||
<Typography key={s.name + s.type + w.name} style={{display: "block"}}>
|
||||
<MythicStyledTooltip title={"Fetch Container Metadata"} >
|
||||
<IconButton onClick={() => getIDPMetadata(w.name, s.name)} >
|
||||
<PermIdentityTwoToneIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
{s.name}
|
||||
</Typography>
|
||||
))}
|
||||
</MythicTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{openDelete &&
|
||||
<MythicConfirmDialog onClose={() => { setOpenDeleteDialog(false); }}
|
||||
onSubmit={onAcceptDelete}
|
||||
open={openDelete}
|
||||
acceptText={deletingContainer.current.deleted ? "Restore" : "Remove"}
|
||||
acceptColor={deletingContainer.current.deleted ? "success" : "error"}/>
|
||||
}
|
||||
{openIDPMetadata &&
|
||||
<MythicDialog fullWidth={true} maxWidth="lg" open={openIDPMetadata}
|
||||
onClose={()=>{setOpenIDPMetadata(false);}}
|
||||
innerDialog={<ConsumingServicesGetIDPMetadataDialog
|
||||
container={IDPMetadataRef.current.container}
|
||||
idp={IDPMetadataRef.current.idp}
|
||||
onClose={()=>{setOpenIDPMetadata(false);}} />}
|
||||
/>
|
||||
}
|
||||
{openListFilesDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openListFilesDialog}
|
||||
onClose={()=>{setOpenListFilesDialog(false);}}
|
||||
innerDialog={<C2ProfileListFilesDialog container_name={openListFilesInformation.current}
|
||||
onClose={()=>{setOpenListFilesDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export const ConsumingServicesTableRow = ({service, showDeleted}) => {
|
||||
const theme = useTheme();
|
||||
const openListFilesInformation = React.useRef("");
|
||||
const [testWebhook] = useMutation(testWebhookMutation, {
|
||||
onCompleted: data => {
|
||||
if (data.consumingServicesTestWebhook.status === "success") {
|
||||
snackActions.success("Successfully sent test message to service");
|
||||
} else {
|
||||
console.log(data.consumingServicesTestWebhook.error)
|
||||
snackActions.error("No webhook listening")
|
||||
}
|
||||
|
||||
},
|
||||
onError: error => {
|
||||
|
||||
}
|
||||
});
|
||||
const issueTestWebhook = (service_type) => {
|
||||
testWebhook({variables: {service_type: service_type}});
|
||||
}
|
||||
const [testLog] = useMutation(testLogMutation, {
|
||||
onCompleted: data => {
|
||||
if (data.consumingServicesTestLog.status === "success") {
|
||||
snackActions.success("Successfully sent test message to service");
|
||||
} else {
|
||||
snackActions.error("No logger listening")
|
||||
console.log(data.consumingServicesTestLog.error)
|
||||
}
|
||||
|
||||
},
|
||||
onError: error => {
|
||||
|
||||
}
|
||||
});
|
||||
const issueTestLog = (service_type) => {
|
||||
testLog({variables: {service_type: service_type}});
|
||||
}
|
||||
const [openIDPMetadata, setOpenIDPMetadata] = React.useState(false);
|
||||
const IDPMetadataRef = React.useRef({"container": "", "idp": ""});
|
||||
const [openListFilesDialog, setOpenListFilesDialog] = React.useState(false);
|
||||
const [openDelete, setOpenDeleteDialog] = React.useState(false);
|
||||
const deletingContainer = React.useRef({});
|
||||
const getIDPMetadata = (container, idp) => {
|
||||
IDPMetadataRef.current = {"container": container, "idp": idp};
|
||||
setOpenIDPMetadata(true);
|
||||
}
|
||||
const [updateDeleted] = useMutation(toggleDeleteStatus, {
|
||||
onCompleted: data => {
|
||||
snackActions.success("Successfully updated");
|
||||
},
|
||||
onError: error => {
|
||||
console.log(error);
|
||||
snackActions.error("Failed to update: " + error.message);
|
||||
}
|
||||
});
|
||||
const onAcceptDelete = () => {
|
||||
updateDeleted({variables: {id: deletingContainer.current.id, deleted: !deletingContainer.current.deleted}})
|
||||
setOpenDeleteDialog(false);
|
||||
}
|
||||
const adjustingDelete = (service) => {
|
||||
deletingContainer.current = service;
|
||||
setOpenDeleteDialog(true);
|
||||
}
|
||||
const [localData, setLocalData] = React.useState({...service, subscriptions: []});
|
||||
const onOpenListFilesDialog = (container) => {
|
||||
openListFilesInformation.current = container;
|
||||
setOpenListFilesDialog(true);
|
||||
}
|
||||
React.useEffect(() => {
|
||||
switch(service.type){
|
||||
case "webhook":
|
||||
case "logging":
|
||||
break;
|
||||
case "eventing":
|
||||
try{
|
||||
const newSubs = service.subscriptions.map( s => {
|
||||
try{
|
||||
return JSON.parse(s);
|
||||
}catch(error){
|
||||
console.log(error);
|
||||
return {name: "", description: s};
|
||||
}
|
||||
});
|
||||
setLocalData({...service, subscriptions: newSubs});
|
||||
}catch(error){
|
||||
|
||||
}
|
||||
break;
|
||||
case "auth":
|
||||
try{
|
||||
const newSubs = service.subscriptions.map( s => {
|
||||
try{
|
||||
return JSON.parse(s);
|
||||
}catch(error){
|
||||
console.log(error);
|
||||
return {name: s, type: ""};
|
||||
}
|
||||
});
|
||||
setLocalData({...service, subscriptions: newSubs});
|
||||
}catch(error){
|
||||
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}, [service]);
|
||||
const getTableRow = (w) => {
|
||||
switch(service.type){
|
||||
case "webhook":
|
||||
return (
|
||||
(showDeleted || !w.deleted) &&
|
||||
<TableRow key={w.id} hover>
|
||||
<MythicTableCell>
|
||||
{w.deleted ? (
|
||||
<IconButton onClick={() => {
|
||||
adjustingDelete(w);
|
||||
}} color="success" size="medium">
|
||||
<RestoreFromTrashOutlinedIcon/>
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton onClick={() => {
|
||||
adjustingDelete(w);
|
||||
}} color="error" size="medium">
|
||||
<DeleteIcon/>
|
||||
</IconButton>
|
||||
)}
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<Typography variant={"h5"}>
|
||||
{w.name}
|
||||
</Typography>
|
||||
<Typography variant={"body"}>
|
||||
<b>Type: </b>{w.type}
|
||||
</Typography>
|
||||
<Typography variant={"body2"}>
|
||||
<b>Description: </b>{w.description}
|
||||
</Typography>
|
||||
|
||||
{w.container_running &&
|
||||
<Typography variant="body2" component="p"
|
||||
style={{color: theme.palette.success.main}}>
|
||||
<b>{"Online"}</b>
|
||||
</Typography>
|
||||
}
|
||||
{!w.container_running &&
|
||||
<Typography variant="body2" component="p"
|
||||
style={{color: theme.palette.error.main}}>
|
||||
<b>{"Offline"}</b>
|
||||
</Typography>
|
||||
}
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
|
||||
<IconButton
|
||||
color={"secondary"}
|
||||
disabled={!w.container_running}
|
||||
onClick={()=>{onOpenListFilesDialog(w.name);}}
|
||||
size="medium">
|
||||
<AttachFileIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
{webhook_events.map(s => (
|
||||
<MythicStyledTooltip title={"test webhook " + s} key={w.id + "webhook_" + s}>
|
||||
<IconButton
|
||||
disabled={!w.subscriptions.includes(s)}
|
||||
onClick={() => {
|
||||
issueTestWebhook(s)
|
||||
}}
|
||||
size="medium">
|
||||
<PublicIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
))}
|
||||
</MythicTableCell>
|
||||
</TableRow>
|
||||
)
|
||||
case "logging":
|
||||
return (
|
||||
(showDeleted || !w.deleted) &&
|
||||
<TableRow key={w.id} hover>
|
||||
<MythicTableCell>
|
||||
{w.deleted ? (
|
||||
<IconButton onClick={() => {
|
||||
adjustingDelete(w);
|
||||
}} color="success" size="medium">
|
||||
<RestoreFromTrashOutlinedIcon/>
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton onClick={() => {
|
||||
adjustingDelete(w);
|
||||
}} color="error" size="medium">
|
||||
<DeleteIcon/>
|
||||
</IconButton>
|
||||
)}
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<Typography variant={"h5"}>
|
||||
{w.name}
|
||||
</Typography>
|
||||
<Typography variant={"body"}>
|
||||
<b>Type: </b>{w.type}
|
||||
</Typography>
|
||||
<Typography variant={"body2"}>
|
||||
<b>Description: </b>{w.description}
|
||||
</Typography>
|
||||
{w.container_running &&
|
||||
<Typography variant="body2" component="p"
|
||||
style={{color: theme.palette.success.main}}>
|
||||
<b>{"Online"}</b>
|
||||
</Typography>
|
||||
}
|
||||
{!w.container_running &&
|
||||
<Typography variant="body2" component="p"
|
||||
style={{color: theme.palette.error.main}}>
|
||||
<b>{"Offline"}</b>
|
||||
</Typography>
|
||||
}
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
|
||||
<IconButton
|
||||
color={"secondary"}
|
||||
disabled={!w.container_running}
|
||||
onClick={()=>{onOpenListFilesDialog(w.name);}}
|
||||
size="medium">
|
||||
<AttachFileIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
{logging_events.map(s => (
|
||||
<MythicStyledTooltip title={"test logging " + s} key={w.id + "logging_" + s}>
|
||||
<IconButton
|
||||
disabled={!w.subscriptions.includes(s)}
|
||||
onClick={() => {
|
||||
issueTestLog(s)
|
||||
}}
|
||||
size="medium">
|
||||
<SyncAltIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
))}
|
||||
</MythicTableCell>
|
||||
</TableRow>
|
||||
)
|
||||
case "eventing":
|
||||
return (
|
||||
(showDeleted || !w.deleted) &&
|
||||
<TableRow key={w.id} hover>
|
||||
<MythicTableCell>
|
||||
{w.deleted ? (
|
||||
<IconButton onClick={() => {
|
||||
adjustingDelete(w);
|
||||
}} color="success" size="medium">
|
||||
<RestoreFromTrashOutlinedIcon/>
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton onClick={() => {
|
||||
adjustingDelete(w);
|
||||
}} color="error" size="medium">
|
||||
<DeleteIcon/>
|
||||
</IconButton>
|
||||
)}
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<Typography variant={"h5"}>
|
||||
{w.name}
|
||||
</Typography>
|
||||
<Typography variant={"body"}>
|
||||
<b>Type: </b>{w.type}
|
||||
</Typography>
|
||||
<Typography variant={"body2"}>
|
||||
<b>Description: </b>{w.description}
|
||||
</Typography>
|
||||
{w.container_running &&
|
||||
<Typography variant="body2" component="p"
|
||||
style={{color: theme.palette.success.main}}>
|
||||
<b>{"Online"}</b>
|
||||
</Typography>
|
||||
}
|
||||
{!w.container_running &&
|
||||
<Typography variant="body2" component="p"
|
||||
style={{color: theme.palette.error.main}}>
|
||||
<b>{"Offline"}</b>
|
||||
</Typography>
|
||||
}
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
|
||||
<IconButton
|
||||
color={"secondary"}
|
||||
disabled={!w.container_running}
|
||||
onClick={()=>{onOpenListFilesDialog(w.name);}}
|
||||
size="medium">
|
||||
<AttachFileIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Function</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{w.subscriptions.map(s => (
|
||||
<TableRow key={s.name} hover>
|
||||
<MythicTableCell><b>{s.name}</b></MythicTableCell>
|
||||
<MythicTableCell>{s.description}</MythicTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
</MythicTableCell>
|
||||
</TableRow>
|
||||
)
|
||||
case "auth":
|
||||
return (
|
||||
(showDeleted || !w.deleted) &&
|
||||
<TableRow key={w.id} hover>
|
||||
<MythicTableCell>
|
||||
{w.deleted ? (
|
||||
<IconButton onClick={() => {
|
||||
adjustingDelete(w);
|
||||
}} color="success" size="medium">
|
||||
<RestoreFromTrashOutlinedIcon/>
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton onClick={() => {
|
||||
adjustingDelete(w);
|
||||
}} color="error" size="medium">
|
||||
<DeleteIcon/>
|
||||
</IconButton>
|
||||
)}
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<Typography variant={"h5"}>
|
||||
{w.name}
|
||||
</Typography>
|
||||
<Typography variant={"body"}>
|
||||
<b>Type: </b>{w.type}
|
||||
</Typography>
|
||||
<Typography variant={"body2"}>
|
||||
<b>Description: </b>{w.description}
|
||||
</Typography>
|
||||
{w.container_running &&
|
||||
<Typography variant="body2" component="p"
|
||||
style={{color: theme.palette.success.main}}>
|
||||
<b>{"Online"}</b>
|
||||
</Typography>
|
||||
}
|
||||
{!w.container_running &&
|
||||
<Typography variant="body2" component="p"
|
||||
style={{color: theme.palette.error.main}}>
|
||||
<b>{"Offline"}</b>
|
||||
</Typography>
|
||||
}
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
|
||||
<IconButton
|
||||
color={"secondary"}
|
||||
disabled={!w.container_running}
|
||||
onClick={()=>{onOpenListFilesDialog(w.name);}}
|
||||
size="medium">
|
||||
<AttachFileIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
{w.subscriptions.map(s => (
|
||||
<Typography key={s.name + s.type + w.name} style={{display: "block"}}>
|
||||
<MythicStyledTooltip title={"Fetch Container Metadata"} >
|
||||
<IconButton onClick={() => getIDPMetadata(w.name, s.name)} >
|
||||
<PermIdentityTwoToneIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
{s.name}
|
||||
</Typography>
|
||||
))}
|
||||
</MythicTableCell>
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{getTableRow(localData)}
|
||||
{openDelete &&
|
||||
<MythicConfirmDialog onClose={() => { setOpenDeleteDialog(false); }}
|
||||
onSubmit={onAcceptDelete}
|
||||
open={openDelete}
|
||||
acceptText={deletingContainer.current.deleted ? "Restore" : "Remove"}
|
||||
acceptColor={deletingContainer.current.deleted ? "success" : "error"}/>
|
||||
}
|
||||
{openListFilesDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openListFilesDialog}
|
||||
onClose={()=>{setOpenListFilesDialog(false);}}
|
||||
innerDialog={<C2ProfileListFilesDialog container_name={openListFilesInformation.current}
|
||||
onClose={()=>{setOpenListFilesDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
{openIDPMetadata &&
|
||||
<MythicDialog fullWidth={true} maxWidth="lg" open={openIDPMetadata}
|
||||
onClose={()=>{setOpenIDPMetadata(false);}}
|
||||
innerDialog={<ConsumingServicesGetIDPMetadataDialog
|
||||
container={IDPMetadataRef.current.container}
|
||||
idp={IDPMetadataRef.current.idp}
|
||||
onClose={()=>{setOpenIDPMetadata(false);}} />}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@ import Stepper from '@mui/material/Stepper';
|
||||
import Step from '@mui/material/Step';
|
||||
import StepLabel from '@mui/material/StepLabel';
|
||||
import {Step1SelectOS} from './Step1SelectOS';
|
||||
import {Step2SelectPayloadType} from './Step2SelectPayloadType';
|
||||
import {Step3SelectCommands} from './Step3SelectCommands';
|
||||
import {Step4C2Profiles} from './Step4C2Profiles';
|
||||
import {Step5Build} from './Step5Build';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { snackActions } from '../../utilities/Snackbar';
|
||||
|
||||
function getSteps(){
|
||||
return ['Select Target OS', 'Payload Type', 'Select Commands', 'Select C2 Profiles', 'Build']
|
||||
return ['Select Target OS', 'Configure Payload', 'Select Commands', 'Select C2 Profiles', 'Build']
|
||||
}
|
||||
|
||||
export function CreatePayload(props){
|
||||
@@ -21,21 +21,30 @@ export function CreatePayload(props){
|
||||
const getStepContent = (step) => {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return <Step1SelectOS buildOptions={payload} prevData={payload[0]} finished={handleStepData} canceled={cancelStep} first={true} last={false}/>;
|
||||
return <Step1SelectOS buildOptions={payload} prevData={payload[0]} setAllData={setAllData} finished={handleStepData} canceled={cancelStep} first={true} last={false}/>;
|
||||
case 1:
|
||||
return <Step2SelectPayloadType buildOptions={payload[0]} prevData={payload[1]} finished={handleStepData} canceled={cancelStep} first={false} last={false}/>;
|
||||
return <Step1SelectOS buildOptions={payload} prevData={payload[1]} finished={handleStepData} canceled={cancelStep} first={false} last={false}/>;
|
||||
case 2:
|
||||
return <Step3SelectCommands buildOptions={payload[1]} prevData={payload[2]} finished={handleStepData} canceled={cancelStep} first={false} last={false} />;
|
||||
case 3:
|
||||
return <Step4C2Profiles buildOptions={payload[1]} prevData={payload[3]} finished={handleStepData} canceled={cancelStep} first={false} last={false} />;
|
||||
case 4:
|
||||
return <Step5Build me={me} buildOptions={payload} canceled={cancelStep} first={false} last={true} startOver={startOver} />;
|
||||
return <Step5Build me={me} buildOptions={payload} moveToStep={moveToStep} canceled={cancelStep} first={false} last={true} startOver={startOver} />;
|
||||
default:
|
||||
return 'Unknown step';
|
||||
}
|
||||
}
|
||||
const handleStepData = (stepData) => {
|
||||
setPayload({...payload, [activeStep]: stepData});
|
||||
const setAllData = (stepData) => {
|
||||
setPayload(stepData);
|
||||
handleNext();
|
||||
}
|
||||
const handleStepData = (stepData, clearNextPreviousData) => {
|
||||
let newPayload = {...payload, [activeStep]: stepData};
|
||||
if(clearNextPreviousData){
|
||||
newPayload[activeStep+1] = undefined;
|
||||
}
|
||||
setPayload(newPayload);
|
||||
//console.log(activeStep, newPayload);
|
||||
handleNext();
|
||||
}
|
||||
const cancelStep = () => {
|
||||
@@ -53,6 +62,9 @@ export function CreatePayload(props){
|
||||
const startOver = () => {
|
||||
setActiveStep(0);
|
||||
}
|
||||
const moveToStep = (step) => {
|
||||
setActiveStep(step);
|
||||
}
|
||||
React.useEffect( () => {
|
||||
if(noOperation){
|
||||
snackActions.error("No current operation set! Set a current operation to continue");
|
||||
@@ -60,34 +72,40 @@ export function CreatePayload(props){
|
||||
}, [noOperation])
|
||||
|
||||
return (
|
||||
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%"}} >
|
||||
<Stepper activeStep={activeStep} alternativeLabel style={{marginTop: "10px"}}>
|
||||
{steps.map((label, index) => (
|
||||
<Step key={label}
|
||||
sx={{
|
||||
'& .MuiStepLabel-root .Mui-completed': {
|
||||
color: 'success.main', // circle color (COMPLETED)
|
||||
},
|
||||
'& .MuiStepLabel-label.Mui-completed.MuiStepLabel-alternativeLabel':
|
||||
{
|
||||
color: 'grey.500', // Just text label (COMPLETED)
|
||||
},
|
||||
'& .MuiStepLabel-root .Mui-active': {
|
||||
color: 'info.main', // circle color (ACTIVE)
|
||||
},
|
||||
'& .MuiStepLabel-label.Mui-active.MuiStepLabel-alternativeLabel':
|
||||
{
|
||||
fontWeight: "bold", // Just text label (ACTIVE)
|
||||
color: ''
|
||||
},
|
||||
'& .MuiStepLabel-root .Mui-active .MuiStepIcon-text': {
|
||||
fill: 'black', // circle's number (ACTIVE)
|
||||
},
|
||||
}}>
|
||||
<StepLabel>{label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%"}}>
|
||||
<div style={{display: "flex", flexDirection: "row", width: "100%", alignItems: "center"}}>
|
||||
<Typography variant="h5" style={{marginLeft: "10px", width: "25%"}}>
|
||||
Payload Creation
|
||||
</Typography>
|
||||
<Stepper activeStep={activeStep} alternativeLabel style={{marginTop: "10px", width: "100%"}}>
|
||||
{steps.map((label, index) => (
|
||||
<Step key={label}
|
||||
sx={{
|
||||
'& .MuiStepLabel-root .Mui-completed': {
|
||||
color: 'success.main', // circle color (COMPLETED)
|
||||
},
|
||||
'& .MuiStepLabel-label.Mui-completed.MuiStepLabel-alternativeLabel':
|
||||
{
|
||||
color: 'grey.500', // Just text label (COMPLETED)
|
||||
},
|
||||
'& .MuiStepLabel-root .Mui-active': {
|
||||
color: 'info.main', // circle color (ACTIVE)
|
||||
},
|
||||
'& .MuiStepLabel-label.Mui-active.MuiStepLabel-alternativeLabel':
|
||||
{
|
||||
fontWeight: "bold", // Just text label (ACTIVE)
|
||||
color: ''
|
||||
},
|
||||
'& .MuiStepLabel-root .Mui-active .MuiStepIcon-text': {
|
||||
fill: 'black', // circle's number (ACTIVE)
|
||||
},
|
||||
}}>
|
||||
<StepLabel>{label}</StepLabel>
|
||||
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
</div>
|
||||
<div style={{display: "flex", flexDirection: "column", flexGrow: 1, overflowY: 'auto'}}>
|
||||
{getStepContent(activeStep)}
|
||||
</div>
|
||||
|
||||
+42
-16
@@ -5,26 +5,52 @@ 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 Typography from '@mui/material/Typography';
|
||||
import { CreatePayloadParameter } from './CreatePayloadParameter';
|
||||
import {GetGroupedParameters} from "./Step1SelectOS";
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
|
||||
|
||||
export function CreatePayloadBuildParametersTable(props){
|
||||
|
||||
const theme = useTheme();
|
||||
const buildParameters = GetGroupedParameters({
|
||||
buildParameters: props.buildParameters,
|
||||
os: props.os,
|
||||
c2_name: props.c2_name,
|
||||
});
|
||||
return (
|
||||
<TableContainer className="mythicElement">
|
||||
<Table stickyHeader={true} size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{width: "30%"}}>Build Parameter</TableCell>
|
||||
<TableCell>Value</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{props.buildParameters.map( (op) => (
|
||||
<CreatePayloadParameter key={"buildparamtablerow" + op.id} onChange={props.onChange} {...op} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<div style={{height: "100%", overflowY: "auto", width: "100%"}}>
|
||||
{buildParameters.map(b => (
|
||||
b.parameters.length > 0 &&
|
||||
<span key={b?.name || 'undefined'}>
|
||||
{b.name !== '' && b.name !== undefined &&
|
||||
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", }} variant={"elevation"}>
|
||||
<Typography variant="h6" style={{textAlign: "left", display: "inline-block", marginLeft: "20px", color: theme.pageHeaderColor}}>
|
||||
{b.name}
|
||||
</Typography>
|
||||
</Paper>
|
||||
}
|
||||
<TableContainer className="mythicElement">
|
||||
<Table stickyHeader={true} size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{width: "30%"}}>Build Parameter</TableCell>
|
||||
<TableCell>Value</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{b.parameters.map( (op) => (
|
||||
<CreatePayloadParameter selected_os={props.os} key={"buildparamtablerow" + op.id}
|
||||
payload_type={props.payload_type}
|
||||
instance_name={props.instance_name} onChange={props.onChange} {...op} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
+3
-7
@@ -1,11 +1,10 @@
|
||||
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 { CreatePayloadParameter } from './CreatePayloadParameter';
|
||||
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
|
||||
|
||||
|
||||
export function CreatePayloadC2ProfileParametersTable(props){
|
||||
@@ -14,13 +13,11 @@ export function CreatePayloadC2ProfileParametersTable(props){
|
||||
props.onChange(props.name, paramName, value, error);
|
||||
}
|
||||
return (
|
||||
<TableContainer >
|
||||
|
||||
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "auto"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{width: "20%"}}>Parameter</TableCell>
|
||||
<TableCell>Value</TableCell>
|
||||
<MythicStyledTableCell style={{width: "20%"}}>Parameter</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>Value</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -29,6 +26,5 @@ export function CreatePayloadC2ProfileParametersTable(props){
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
@@ -14,11 +14,13 @@ export function CreatePayloadNavigationButtons(props){
|
||||
const disabledButtons = (me?.user?.current_operation_id || 0) <= 0;
|
||||
return (
|
||||
|
||||
<div >
|
||||
<div style={{marginLeft: "20px"}}>
|
||||
<Button
|
||||
disabled={props.first}
|
||||
color="primary"
|
||||
color="info"
|
||||
variant={"outlined"}
|
||||
onClick={props.canceled}
|
||||
style={{marginRight: "10px"}}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, {useEffect} from 'react';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import Select from '@mui/material/Select';
|
||||
@@ -20,39 +19,188 @@ import MythicStyledTableCell from '../../MythicComponents/MythicTableCell';
|
||||
import {Typography} from '@mui/material';
|
||||
import {MythicFileContext} from "../../MythicComponents/MythicFileContext";
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import {gql, useMutation} from '@apollo/client';
|
||||
import { Backdrop } from '@mui/material';
|
||||
import {CircularProgress} from '@mui/material';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import {DragAndDropFileUpload} from "../Callbacks/TaskParametersDialogRow";
|
||||
|
||||
export function CreatePayloadParameter({onChange, parameter_type, default_value, name, required, verifier_regex, id, description, initialValue, choices, trackedValue, returnAllDictValues}){
|
||||
export const getDynamicQueryBuildParameterParams = gql`
|
||||
mutation getDynamicBuildParamsMutation($payload_type: String!, $parameter_name: String!, $selected_os: String!){
|
||||
dynamicQueryBuildParameterFunction(payload_type: $payload_type, parameter_name: $parameter_name, selected_os: $selected_os){
|
||||
status
|
||||
error
|
||||
choices
|
||||
parameter_name
|
||||
}
|
||||
}
|
||||
`;
|
||||
function isTrue(value){
|
||||
if(typeof value === 'boolean'){
|
||||
return value;
|
||||
}
|
||||
if(typeof value === 'string'){
|
||||
return value.toLowerCase() === 'true' || value.toLowerCase() === 't';
|
||||
}
|
||||
console.log("unknown boolean value", value);
|
||||
}
|
||||
export function CreatePayloadParameter({onChange, parameter_type, default_value, name, required, verifier_regex, id,
|
||||
description, initialValue, choices, trackedValue, instance_name,
|
||||
payload_type, selected_os, dynamic_query_function}){
|
||||
const theme = useTheme();
|
||||
const [value, setValue] = React.useState("");
|
||||
const [valueNum, setValueNum] = React.useState(0);
|
||||
const [multiValue, setMultiValue] = React.useState([]);
|
||||
const [ChoiceOptions, setChoiceOptions] = React.useState([]);
|
||||
const [chooseOneCustomValue, setChooseOneCustomValue] = React.useState("");
|
||||
const [dictValue, setDictValue] = React.useState([]);
|
||||
const [dictOptions, setDictOptions] = React.useState([]);
|
||||
const [dictSelectOptions, setDictSelectOptions] = React.useState([]);
|
||||
const [dictSelectOptionsChoice, setDictSelectOptionsChoice] = React.useState("");
|
||||
const [chooseOptions, setChooseOptions] = React.useState([]);
|
||||
const [dateValue, setDateValue] = React.useState(dayjs(new Date()));
|
||||
const [arrayValue, setArrayValue] = React.useState([""]);
|
||||
const [arrayValue, setArrayValue] = React.useState([]);
|
||||
const [typedArrayValue, setTypedArrayValue] = React.useState([]);
|
||||
const [fileValue, setFileValue] = React.useState({name: ""});
|
||||
const [fileMultValue, setFileMultValue] = React.useState([]);
|
||||
const [mapArray, setMapArray] = React.useState([]);
|
||||
const [backdropOpen, setBackdropOpen] = React.useState(false);
|
||||
const usingDynamicParamChoices = React.useRef(false);
|
||||
const [getDynamicParams] = useMutation(getDynamicQueryBuildParameterParams, {
|
||||
onCompleted: (data) => {
|
||||
if(data.dynamicQueryBuildParameterFunction.status === "success"){
|
||||
try{
|
||||
let choicesInUse = [];
|
||||
usingDynamicParamChoices.current = true;
|
||||
setChoiceOptions([...data.dynamicQueryBuildParameterFunction.choices]);
|
||||
choicesInUse = [...data.dynamicQueryBuildParameterFunction.choices];
|
||||
if(parameter_type === "ChooseOne"){
|
||||
if(choicesInUse.length > 0){
|
||||
if(value !== "") {
|
||||
setValue(value);
|
||||
onChange(name, value, false);
|
||||
} else if(choicesInUse.includes(default_value)) {
|
||||
setValue(default_value);
|
||||
onChange(name, default_value, false);
|
||||
} else {
|
||||
setValue(choicesInUse[0]);
|
||||
onChange(name, choicesInUse[0], false);
|
||||
}
|
||||
}
|
||||
} else if(parameter_type === "ChooseOneCustom"){
|
||||
let newStandardValue = default_value;
|
||||
if(choicesInUse.includes(default_value) && value !== "") {
|
||||
setValue(default_value);
|
||||
} else {
|
||||
setValue(choicesInUse[0]);
|
||||
newStandardValue = choicesInUse[0];
|
||||
}
|
||||
if(!choicesInUse.includes(value) && value !== "" ){
|
||||
setChooseOneCustomValue(value);
|
||||
newStandardValue = value;
|
||||
}
|
||||
onChange(name, newStandardValue, false);
|
||||
} else if(parameter_type === "ChooseMultiple"){
|
||||
if(choicesInUse.length > 0){
|
||||
if(multiValue.length > 0) {
|
||||
setMultiValue(multiValue);
|
||||
onChange(name, multiValue, false);
|
||||
} else if(choicesInUse.includes(default_value)) {
|
||||
setMultiValue(default_value);
|
||||
onChange(name, default_value, false);
|
||||
} else {
|
||||
setMultiValue([choicesInUse[0]]);
|
||||
onChange(name, [choicesInUse[0]], false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}catch(error){
|
||||
setBackdropOpen(false);
|
||||
snackActions.warning("Failed to parse dynamic parameter results");
|
||||
}
|
||||
|
||||
}else{
|
||||
snackActions.warning(data.dynamicQueryBuildParameterFunction.error);
|
||||
}
|
||||
setBackdropOpen(false);
|
||||
},
|
||||
onError: (data) => {
|
||||
snackActions.warning("Failed to perform dynamic parameter query");
|
||||
console.log(data);
|
||||
setBackdropOpen(false);
|
||||
}
|
||||
});
|
||||
const reIssueDynamicQueryFunction = () => {
|
||||
setBackdropOpen(true);
|
||||
snackActions.info("Querying payload type container for options...", {autoClose: 1000});
|
||||
getDynamicParams({variables:{
|
||||
parameter_name: name,
|
||||
selected_os: selected_os,
|
||||
payload_type: payload_type,
|
||||
}})
|
||||
usingDynamicParamChoices.current = true;
|
||||
}
|
||||
const arrayMapToMap = (val) => {
|
||||
return val.reduce( ( prev, cur ) => {
|
||||
return {...prev, [cur[0]]: cur[1]}
|
||||
}, {})
|
||||
}
|
||||
const addMapArrayMap = () => {
|
||||
let map = [...mapArray, ["", []]];
|
||||
setMapArray(map);
|
||||
onChange(name, arrayMapToMap(map), false);
|
||||
}
|
||||
const updateMapArrayMap = (i, val) => {
|
||||
let map = [...mapArray];
|
||||
map[i][0] = val;
|
||||
setMapArray(map);
|
||||
onChange(name, arrayMapToMap(map), false);
|
||||
}
|
||||
const removeMapArrayMap = (i) => {
|
||||
let map = mapArray.toSpliced(i, 1)
|
||||
setMapArray(map);
|
||||
onChange(name, arrayMapToMap(map), false);
|
||||
}
|
||||
const addMapArray = (i) => {
|
||||
let map = [...mapArray];
|
||||
map[i][1].push("");
|
||||
setMapArray(map);
|
||||
onChange(name, arrayMapToMap(map), false);
|
||||
}
|
||||
const updateMapArray = (i, j, val) => {
|
||||
let map = [...mapArray];
|
||||
map[i][1][j] = val;
|
||||
setMapArray(map);
|
||||
onChange(name, arrayMapToMap(map), false);
|
||||
}
|
||||
const removeMapArray = (i, j, val) => {
|
||||
let map = [...mapArray];
|
||||
map[i][1].splice(j, 1);
|
||||
setMapArray(map);
|
||||
onChange(name, arrayMapToMap(map), false);
|
||||
}
|
||||
const submitDictChange = (list) => {
|
||||
onChange(name, list, false);
|
||||
};
|
||||
const onFileChange = (evt) => {
|
||||
setFileValue({name: evt.target.files[0].name});
|
||||
onChange(name, evt.target.files[0]);
|
||||
const onFileChange = (newFile) => {
|
||||
setFileValue({name: newFile.name});
|
||||
onChange(name, newFile);
|
||||
}
|
||||
const onFileMultChange = (evt) => {
|
||||
setFileMultValue([...evt.target.files]);
|
||||
onChange(name, [...evt.target.files]);
|
||||
const onFileMultChange = (newFiles) => {
|
||||
setFileMultValue([...newFiles]);
|
||||
onChange(name, [...newFiles]);
|
||||
}
|
||||
useEffect( () => {
|
||||
if(parameter_type === "ChooseOne" || parameter_type === "ChooseOneCustom"){
|
||||
setChooseOptions(choices);
|
||||
if( parameter_type === "ChooseOne" || parameter_type === "ChooseOneCustom" ){
|
||||
setChoiceOptions(choices);
|
||||
if(!choices.includes(trackedValue)){
|
||||
setChooseOneCustomValue(trackedValue);
|
||||
setValue(default_value);
|
||||
if(dynamic_query_function !== ""){
|
||||
setValue(trackedValue);
|
||||
} else {
|
||||
setValue(default_value);
|
||||
}
|
||||
} else {
|
||||
setValue(trackedValue);
|
||||
setChooseOneCustomValue("");
|
||||
@@ -63,7 +211,7 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
|
||||
setValue(trackedValue);
|
||||
}else if(parameter_type === "ChooseMultiple") {
|
||||
setMultiValue(trackedValue);
|
||||
setChooseOptions(choices);
|
||||
setChoiceOptions(choices);
|
||||
}else if(parameter_type === "File") {
|
||||
if (typeof trackedValue === "string") {
|
||||
setFileValue({name: trackedValue, legacy: trackedValue !== ""});
|
||||
@@ -112,12 +260,30 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
|
||||
setValue( trackedValue );
|
||||
}else if(parameter_type === "Array") {
|
||||
setArrayValue(trackedValue);
|
||||
}else if(parameter_type === "TypedArray"){
|
||||
}else if(parameter_type === "TypedArray") {
|
||||
setTypedArrayValue(trackedValue);
|
||||
}else if(parameter_type === "MapArray") {
|
||||
let initial = [];
|
||||
for(const [key, val] of Object.entries(trackedValue)){
|
||||
initial.push([key, val]);
|
||||
}
|
||||
setMapArray(initial);
|
||||
}else{
|
||||
console.log("hit an unknown parameter type")
|
||||
}
|
||||
}, [default_value, parameter_type, name]);
|
||||
if(dynamic_query_function !== "" && dynamic_query_function !== undefined){
|
||||
if(!usingDynamicParamChoices.current){
|
||||
setBackdropOpen(true);
|
||||
snackActions.info("Querying payload type container for options...", {autoClose: 1000});
|
||||
getDynamicParams({variables:{
|
||||
parameter_name: name,
|
||||
payload_type: payload_type,
|
||||
selected_os: selected_os
|
||||
}})
|
||||
}
|
||||
usingDynamicParamChoices.current = true;
|
||||
}
|
||||
}, [default_value, parameter_type, name, instance_name]);
|
||||
const onChangeTextChooseOneCustom = (name, newValue, error) => {
|
||||
setChooseOneCustomValue(newValue);
|
||||
if(newValue === ""){
|
||||
@@ -207,7 +373,7 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
|
||||
}
|
||||
const removeDictEntry = (i) => {
|
||||
const newValues = dictValue.filter( (opt, index) => {
|
||||
if(i === index){return false};
|
||||
if(i === index){return false}
|
||||
return true;
|
||||
});
|
||||
setDictValue(newValues);
|
||||
@@ -309,53 +475,52 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
|
||||
case "FileMultiple":
|
||||
return (
|
||||
<>
|
||||
<Button variant="contained" component="label">
|
||||
Select Files
|
||||
<input onChange={onFileMultChange} type="file" hidden multiple />
|
||||
</Button>
|
||||
{ fileMultValue.length > 0 &&
|
||||
fileMultValue?.map((f, i) => (
|
||||
<div key={i} style={{display: "inline-block"}}>
|
||||
{typeof f === "string" && <MythicFileContext agent_file_id={f}
|
||||
extraStyles={{bottom: "-10px", position: "relative", marginLeft: "5px", marginRight: "5px"}} />}
|
||||
{typeof f !== "string" && (f.name)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
<DragAndDropFileUpload values={fileMultValue} multiple={true} onChange={onFileMultChange} />
|
||||
</>
|
||||
)
|
||||
case "File":
|
||||
return (
|
||||
<>
|
||||
<Button variant="contained" component="label">
|
||||
{ fileValue.legacy ? "Select New File" : fileValue.name === "" ? "Select File" : fileValue.name }
|
||||
<input onChange={onFileChange} type="file" hidden />
|
||||
</Button>
|
||||
{fileValue.legacy &&
|
||||
<MythicFileContext agent_file_id={fileValue.name}
|
||||
extraStyles={{bottom: "-10px", position: "relative", marginLeft: "5px", marginRight: "5px"}} />
|
||||
}
|
||||
<DragAndDropFileUpload value={fileValue} multiple={false} onChange={onFileChange} />
|
||||
</>
|
||||
|
||||
)
|
||||
case "ChooseOne":
|
||||
return (
|
||||
<FormControl>
|
||||
<Select
|
||||
value={value}
|
||||
onChange={onChangeValue}
|
||||
>
|
||||
{
|
||||
chooseOptions.map((opt, i) => (
|
||||
<MenuItem key={"buildparamopt" + i} value={opt}>{opt}</MenuItem>
|
||||
))
|
||||
<div style={{position: "relative", display: "flex", alignItems: "center", overflow: "hidden"}}>
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={false}>
|
||||
<CircularProgress color="inherit" />
|
||||
</Backdrop>
|
||||
<FormControl style={{width: "100%"}}>
|
||||
{ChoiceOptions.length === 0 &&
|
||||
<InputLabel>{"No Options Available"}</InputLabel>
|
||||
}
|
||||
<Select
|
||||
value={value}
|
||||
onChange={onChangeValue}
|
||||
>
|
||||
{
|
||||
ChoiceOptions.map((opt, i) => (
|
||||
<MenuItem key={"buildparamopt" + i} value={opt}>{opt}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{dynamic_query_function !== "" && dynamic_query_function !== undefined &&
|
||||
<MythicStyledTooltip title={"ReIssue Dynamic Query Function"} tooltipStyle={{display: "inline-block"}}>
|
||||
<IconButton onClick={reIssueDynamicQueryFunction}>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
);
|
||||
case "ChooseOneCustom":
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div style={{position: "relative"}}>
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={false}>
|
||||
<CircularProgress color="inherit" />
|
||||
</Backdrop>
|
||||
<div style={{width: "100%", display: "flex", alignItems: "center"}}>
|
||||
<FormControl style={{width: "20%"}}>
|
||||
<Select
|
||||
@@ -366,7 +531,7 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
|
||||
input={<Input />}
|
||||
>
|
||||
{
|
||||
chooseOptions.map((opt, i) => (
|
||||
ChoiceOptions.map((opt, i) => (
|
||||
<MenuItem key={name + i} value={opt}>{opt}</MenuItem>
|
||||
))
|
||||
}
|
||||
@@ -376,25 +541,48 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
|
||||
<MythicTextField name={name} requiredValue={required} placeholder={"Custom Value"} value={chooseOneCustomValue} multiline={true} maxRows={5}
|
||||
onChange={onChangeTextChooseOneCustom} display="inline-block"
|
||||
/>
|
||||
{dynamic_query_function !== "" && dynamic_query_function !== undefined &&
|
||||
<MythicStyledTooltip title={"ReIssue Dynamic Query Function"} tooltipStyle={{display: "inline-block"}}>
|
||||
<IconButton onClick={reIssueDynamicQueryFunction}>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
</div>
|
||||
|
||||
</React.Fragment>
|
||||
</div>
|
||||
)
|
||||
case "ChooseMultiple":
|
||||
return (
|
||||
<FormControl>
|
||||
<Select
|
||||
value={multiValue}
|
||||
multiple={true}
|
||||
onChange={onChangeMultValue}
|
||||
>
|
||||
{
|
||||
chooseOptions.map((opt, i) => (
|
||||
<MenuItem key={"buildparamopt" + i} value={opt}>{opt}</MenuItem>
|
||||
))
|
||||
<div style={{position: "relative", display: "flex", alignItems: "center", overflow: "hidden"}}>
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={false}>
|
||||
<CircularProgress color="inherit" />
|
||||
</Backdrop>
|
||||
<FormControl style={{width: "100%"}}>
|
||||
<InputLabel id={name + "select"} style={{paddingTop: "15px"}}>{"Select Multiple"}</InputLabel>
|
||||
{ChoiceOptions.length === 0 &&
|
||||
<InputLabel>{"No Options Available"}</InputLabel>
|
||||
}
|
||||
<Select
|
||||
value={multiValue}
|
||||
multiple={true}
|
||||
onChange={onChangeMultValue}
|
||||
>
|
||||
{
|
||||
ChoiceOptions.map((opt, i) => (
|
||||
<MenuItem key={"buildparamopt" + i} value={opt}>{opt}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{dynamic_query_function !== "" && dynamic_query_function !== undefined &&
|
||||
<MythicStyledTooltip title={"ReIssue Dynamic Query Function"} tooltipStyle={{display: "inline-block"}}>
|
||||
<IconButton onClick={reIssueDynamicQueryFunction}>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
);
|
||||
case "Array":
|
||||
return (
|
||||
@@ -410,7 +598,7 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<MythicTextField requiredValue={required} fullWidth={true} placeholder={""} value={a} multiline={true}
|
||||
onChange={(n,v,e) => onChangeArrayText(v, e, i)} display="inline-block" autoFocus={a === ""}
|
||||
onChange={(n,v,e) => onChangeArrayText(v, e, i)} display="inline-block" autoFocus={i !== 0 && a === ""}
|
||||
validate={testParameterValues} errorText={"Must match: " + verifier_regex} marginBottom={"0px"}
|
||||
/>
|
||||
</MythicStyledTableCell>
|
||||
@@ -514,11 +702,64 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
|
||||
return (
|
||||
<Switch
|
||||
color={"info"}
|
||||
checked={Boolean(value)}
|
||||
checked={isTrue(value)}
|
||||
onChange={toggleSwitchValue}
|
||||
inputProps={{ 'aria-label': 'info checkbox' }}
|
||||
/>
|
||||
);
|
||||
case "MapArray":
|
||||
return (
|
||||
<>
|
||||
<Table size="small" style={{tableLayout: "fixed", maxWidth: "100%", height: "100%", "overflow": "auto"}}>
|
||||
<TableBody>
|
||||
{mapArray.map( (val, i) => (
|
||||
<TableRow key={"payloadtype" + i}>
|
||||
<MythicStyledTableCell style={{width: "2rem"}}>
|
||||
<IconButton onClick={(e) => {removeMapArrayMap(i)}} color="error">
|
||||
<DeleteIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<MythicTextField autoFocus={true} name={"Payload Type Name"} onChange={(name, value) => updateMapArrayMap(i, value)} value={val[0]} />
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<Table size="small" style={{tableLayout: "fixed", maxWidth: "100%", height: "100%", "overflow": "auto"}}>
|
||||
<TableBody>
|
||||
{val[1].map( (v, j) => (
|
||||
<TableRow key={"payloadtypevalue" + i + j}>
|
||||
<MythicStyledTableCell style={{width: "2rem"}}>
|
||||
<IconButton onClick={(e) => {removeMapArray(i, j)}} color="error">
|
||||
<DeleteIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<MythicTextField autoFocus={true} name={"Command Name"} onChange={(name, value) => updateMapArray(i, j, value)} value={v}
|
||||
onEnter={() => addMapArray(i)}/>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "3rem"}}>
|
||||
<IconButton onClick={() => addMapArray(i)} size="large"> <AddCircleIcon color="success" /> </IconButton>
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell/>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "3rem"}}>
|
||||
<IconButton onClick={addMapArrayMap} size="large"> <AddCircleIcon color="success" /> </IconButton>
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell/>
|
||||
<MythicStyledTableCell/>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
@@ -557,15 +798,21 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow key={"buildparam" + id}>
|
||||
<TableCell>
|
||||
<TableRow key={"buildparam" + id} hover>
|
||||
<MythicStyledTableCell>
|
||||
<MythicStyledTooltip title={name.length > 0 ? name : "No Description"}>
|
||||
{description}
|
||||
<Typography style={{fontWeight: "600"}} >
|
||||
{name}
|
||||
</Typography>
|
||||
<Typography style={{fontSize: theme.typography.pxToRem(15), marginLeft: "10px"}}>
|
||||
{description}
|
||||
</Typography>
|
||||
|
||||
{modifiedValue() ? (
|
||||
<Typography color="warning.main">Modified</Typography>
|
||||
) : null}
|
||||
</MythicStyledTooltip>
|
||||
</TableCell>
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
{getParameterObject()}
|
||||
</MythicStyledTableCell>
|
||||
|
||||
@@ -1,89 +1,895 @@
|
||||
import React from 'react';
|
||||
import {useQuery, gql} from '@apollo/client';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import {gql} from '@apollo/client';
|
||||
import Select from '@mui/material/Select';
|
||||
import { CreatePayloadNavigationButtons} from './CreatePayloadNavigationButtons';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { snackActions } from '../../utilities/Snackbar';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Button from '@mui/material/Button';
|
||||
import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery";
|
||||
import {PayloadSelect} from "../CreateWrapper/Step3SelectPayload";
|
||||
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
|
||||
import AddCircleIcon from '@mui/icons-material/AddCircle';
|
||||
import {getDefaultChoices, getDefaultValueForType, getSavedToType} from "./Step2SelectPayloadType";
|
||||
import {CreatePayloadBuildParametersTable} from "./CreatePayloadBuildParametersTable";
|
||||
import {ParseForDisplay} from "../Payloads/DetailedPayloadTable";
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import {getModifiedC2Params} from "./Step4C2Profiles";
|
||||
import { Backdrop, CircularProgress } from '@mui/material';
|
||||
|
||||
|
||||
const GET_Payload_Types = gql`
|
||||
query getPayloadTypesQuery {
|
||||
payloadtype(where: {deleted: {_eq: false}, wrapper: {_eq: false}}) {
|
||||
payloadtype(where: {deleted: {_eq: false}, wrapper: {_eq: false}, agent_type: {_in: ["agent", "service"]}}) {
|
||||
id
|
||||
supported_os
|
||||
name
|
||||
note
|
||||
semver
|
||||
payloadtypec2profiles{
|
||||
c2profile{
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GetPayloads = gql`
|
||||
query payloads($payloadType: String!, $os: String!) {
|
||||
payload(where: {auto_generated: {_eq: false}, os: {_eq: $os}, payloadtype: {name: {_eq: $payloadType}}, deleted: {_eq: false}}, order_by: {id: desc}) {
|
||||
id
|
||||
description
|
||||
uuid
|
||||
creation_time
|
||||
build_phase
|
||||
payload_build_steps(order_by: {step_number: asc}) {
|
||||
step_name
|
||||
step_number
|
||||
step_success
|
||||
step_skip
|
||||
start_time
|
||||
end_time
|
||||
step_stdout
|
||||
step_stderr
|
||||
id
|
||||
}
|
||||
filemetum {
|
||||
agent_file_id
|
||||
filename_text
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GetBuildParametersQuery = gql`
|
||||
query getPayloadTypesBuildParametersQuery($payloadtype: String!) {
|
||||
payloadtype(where: {name: {_eq: $payloadtype}}) {
|
||||
name
|
||||
id
|
||||
file_extension
|
||||
agent_type
|
||||
note
|
||||
supports_dynamic_loading
|
||||
supports_multiple_c2_in_build
|
||||
supports_multiple_c2_instances_in_build
|
||||
c2_parameter_deviations
|
||||
semver
|
||||
buildparameters(where: {deleted: {_eq: false} }, order_by: {description: asc}) {
|
||||
default_value
|
||||
description
|
||||
format_string
|
||||
id
|
||||
name
|
||||
parameter_type
|
||||
randomize
|
||||
required
|
||||
verifier_regex
|
||||
choices
|
||||
group_name
|
||||
supported_os
|
||||
hide_conditions
|
||||
ui_position
|
||||
dynamic_query_function
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const GetPayloadBuildQuery = gql`
|
||||
query getPayloadTypesBuildParametersQuery($payload_id: Int!) {
|
||||
payload_by_pk(id: $payload_id) {
|
||||
buildparameterinstances {
|
||||
build_parameter_id
|
||||
value
|
||||
}
|
||||
payloadcommands {
|
||||
command {
|
||||
cmd
|
||||
}
|
||||
}
|
||||
payloadtype {
|
||||
name
|
||||
id
|
||||
file_extension
|
||||
agent_type
|
||||
note
|
||||
supports_dynamic_loading
|
||||
supports_multiple_c2_in_build
|
||||
supports_multiple_c2_instances_in_build
|
||||
c2_parameter_deviations
|
||||
semver
|
||||
buildparameters(where: {deleted: {_eq: false} }, order_by: {description: asc}) {
|
||||
default_value
|
||||
description
|
||||
format_string
|
||||
id
|
||||
name
|
||||
parameter_type
|
||||
randomize
|
||||
required
|
||||
verifier_regex
|
||||
choices
|
||||
group_name
|
||||
supported_os
|
||||
hide_conditions
|
||||
ui_position
|
||||
dynamic_query_function
|
||||
}
|
||||
}
|
||||
c2profileparametersinstances{
|
||||
c2profileparameter {
|
||||
default_value
|
||||
description
|
||||
format_string
|
||||
id
|
||||
name
|
||||
parameter_type
|
||||
randomize
|
||||
required
|
||||
verifier_regex
|
||||
choices
|
||||
ui_position
|
||||
c2profile {
|
||||
name
|
||||
id
|
||||
is_p2p
|
||||
c2profileparametersinstances(where: {instance_name: {_is_null: false}}, distinct_on: instance_name, order_by: {instance_name: asc}){
|
||||
instance_name
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
id
|
||||
value
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function Step1SelectOS(props){
|
||||
const [os, setOS] = React.useState('');
|
||||
const [openBackdrop, setOpenBackdrop] = React.useState(true);
|
||||
const [payloadtypeData, setPayloadtypeData] = React.useState({});
|
||||
const [payloadtypesPerOS, setPayloadtypesPerOS] = React.useState({});
|
||||
const [C2PerOS, setC2PerOS] = React.useState({});
|
||||
const [osOptions, setOSOptions] = React.useState([]);
|
||||
const { loading } = useQuery(GET_Payload_Types, {fetchPolicy: "network-only",
|
||||
onCompleted: (data) => {
|
||||
const optionsReduced= data.payloadtype.reduce((currentOptions, payloadtype) => {
|
||||
const adds = payloadtype.supported_os.reduce( (prev, os) => {
|
||||
if(!currentOptions.includes(os)){
|
||||
return [...prev, os];
|
||||
}
|
||||
return prev;
|
||||
}, []);
|
||||
return [...currentOptions, ...adds];
|
||||
}, []);
|
||||
const sortedOptions = optionsReduced.sort();
|
||||
if(props.prevData !== undefined){
|
||||
setOS(props.prevData);
|
||||
}
|
||||
else if(os === "" && sortedOptions.length > 0){
|
||||
setOS(sortedOptions[0]);
|
||||
} else if(sortedOptions.length === 0){
|
||||
snackActions.error("No Payload Types installed");
|
||||
}
|
||||
setOSOptions(sortedOptions);
|
||||
},
|
||||
onError: (data) => {
|
||||
console.error(data);
|
||||
snackActions.error(data.message)
|
||||
}
|
||||
const [selectedPayloadType, setSelectedPayloadType] = React.useState('');
|
||||
const [payloadOptions, setPayloadOptions] = React.useState([]);
|
||||
const payloadConfigRef = React.useRef({});
|
||||
const getPayloadTypeAndPreviousData = useMythicLazyQuery(GET_Payload_Types, {fetchPolicy: "network-only",
|
||||
});
|
||||
if (loading) {
|
||||
return <div><CircularProgress /></div>;
|
||||
}
|
||||
const finished = () => {
|
||||
if(os === ""){
|
||||
snackActions.error("Must select an operating system first");
|
||||
return;
|
||||
const getPayloads = useMythicLazyQuery(GetPayloads, { fetchPolicy: "network-only",
|
||||
})
|
||||
const getPayloadConfig = useMythicLazyQuery(GetPayloadBuildQuery, { fetchPolicy: "network-only",
|
||||
})
|
||||
React.useEffect( () => {
|
||||
getPayloadTypeAndPreviousData({})
|
||||
.then(({data}) => {
|
||||
let payloadTypeOS = {};
|
||||
let C2OS = {};
|
||||
const payloadTypeData = data.payloadtype.reduce( (prev, cur) => {
|
||||
return {...prev, [cur.name]: {...cur}};
|
||||
}, {});
|
||||
setPayloadtypeData(payloadTypeData);
|
||||
const optionsReduced= data.payloadtype.reduce((currentOptions, payloadtype) => {
|
||||
const adds = payloadtype.supported_os.reduce( (prev, os) => {
|
||||
if(payloadTypeOS[os] === undefined){
|
||||
payloadTypeOS[os] = [payloadtype.name];
|
||||
} else {
|
||||
payloadTypeOS[os].push(payloadtype.name);
|
||||
payloadTypeOS[os].sort();
|
||||
}
|
||||
if(C2OS[os] === undefined){
|
||||
C2OS[os] = [...payloadtype.payloadtypec2profiles.map(c => c.c2profile.name)];
|
||||
} else {
|
||||
let newProfiles = payloadtype.payloadtypec2profiles.map(c => c.c2profile.name);
|
||||
C2OS[os] = C2OS[os].reduce( (prevC2, curC2) => {
|
||||
if(prevC2.includes(curC2)){
|
||||
return [...prevC2];
|
||||
}
|
||||
return [...prevC2, curC2];
|
||||
}, [...newProfiles]).sort();
|
||||
}
|
||||
if(!currentOptions.includes(os)){
|
||||
return [...prev, os];
|
||||
}
|
||||
return prev;
|
||||
}, []);
|
||||
return [...currentOptions, ...adds];
|
||||
}, []);
|
||||
const sortedOptions = optionsReduced.sort();
|
||||
if(props.prevData !== undefined){
|
||||
setOS(props.prevData.os);
|
||||
setSelectedPayloadType(props.prevData.payload_type);
|
||||
}
|
||||
else if(os === "" && sortedOptions.length > 0){
|
||||
setOS(sortedOptions[0]);
|
||||
setSelectedPayloadType(payloadTypeOS[sortedOptions[0]][0]);
|
||||
} else if(sortedOptions.length === 0){
|
||||
snackActions.error("No Payload Types installed");
|
||||
}
|
||||
setPayloadtypesPerOS(payloadTypeOS);
|
||||
setC2PerOS(C2OS);
|
||||
setOSOptions(sortedOptions);
|
||||
})
|
||||
.catch((data) => {console.log(data)})
|
||||
}, [props.prevData, props.first])
|
||||
React.useEffect( () => {
|
||||
setOpenBackdrop(true);
|
||||
getPayloads({variables: {payloadType: selectedPayloadType, os}})
|
||||
.then(({data}) => {
|
||||
setPayloadOptions(data.payload);
|
||||
setOpenBackdrop(false);
|
||||
})
|
||||
.catch(({data}) => {
|
||||
console.log(data);
|
||||
setOpenBackdrop(false);
|
||||
});
|
||||
}, [selectedPayloadType, os]);
|
||||
|
||||
const finished = (clearNextPrevious) => {
|
||||
if(props.first){
|
||||
props.finished({
|
||||
"os": os,
|
||||
"payload_type": selectedPayloadType
|
||||
}, clearNextPrevious);
|
||||
} else {
|
||||
props.finished(os);
|
||||
props.finished({...payloadConfigRef.current});
|
||||
}
|
||||
|
||||
}
|
||||
const canceled = () => {
|
||||
props.canceled();
|
||||
}
|
||||
return (
|
||||
<div style={{height: "100%", display: "flex", flexDirection: "column"}}>
|
||||
<Typography variant="h3" align="left" id="selectospage" component="div"
|
||||
style={{"marginLeft": "10px"}}>
|
||||
Select Target Operating System
|
||||
</Typography> <br/>
|
||||
<div style={{flexGrow: 1, width: "100%", display: "flex", flexDirection: "column"}}>
|
||||
<Select
|
||||
value={os}
|
||||
onChange={evt => setOS(evt.target.value)}
|
||||
>
|
||||
{
|
||||
osOptions.map((opt) => (
|
||||
<MenuItem key={"step1" + opt} value={opt}>{opt}</MenuItem>
|
||||
))
|
||||
const onChangeOS = (e) => {
|
||||
setOS(e.target.value);
|
||||
setSelectedPayloadType(payloadtypesPerOS[e.target.value][0]);
|
||||
}
|
||||
const onSelectedPayload = (payload) => {
|
||||
let newConfig = {
|
||||
0: {
|
||||
"os": os,
|
||||
"payload_type": selectedPayloadType,
|
||||
"payload": payload.id,
|
||||
},
|
||||
};
|
||||
getPayloadConfig({variables: {payload_id: payload.id}})
|
||||
.then(({data}) => {
|
||||
newConfig[1] = {
|
||||
os: os,
|
||||
payload: payload.id,
|
||||
payload_type: selectedPayloadType,
|
||||
agent_type: data.payload_by_pk.payloadtype.agent_type,
|
||||
description: data.payload_by_pk.payloadtype.note,
|
||||
file_extension: data.payload_by_pk.payloadtype.file_extension,
|
||||
supports_dynamic_loading: data.payload_by_pk.payloadtype.supports_dynamic_loading,
|
||||
supports_multiple_c2_in_build: data.payload_by_pk.payloadtype.supports_multiple_c2_in_build,
|
||||
supports_multiple_c2_instances_in_build: data.payload_by_pk.payloadtype.supports_multiple_c2_instances_in_build,
|
||||
c2_parameter_deviations: data.payload_by_pk.payloadtype.c2_parameter_deviations,
|
||||
};
|
||||
const params = data.payload_by_pk.payloadtype.buildparameters.map((param) => {
|
||||
for (let p = 0; p < data.payload_by_pk.buildparameterinstances.length; p++) {
|
||||
if (data.payload_by_pk.buildparameterinstances[p]["build_parameter_id"] === param.id) {
|
||||
let value = {
|
||||
...param, error: false,
|
||||
value: data.payload_by_pk.buildparameterinstances[p]["value"],
|
||||
trackedValue: data.payload_by_pk.buildparameterinstances[p]["value"],
|
||||
initialValue: data.payload_by_pk.buildparameterinstances[p]["value"],
|
||||
choices: getDefaultChoices(param)
|
||||
};
|
||||
let newValue = getSavedToType(value);
|
||||
return {
|
||||
...param, error: false,
|
||||
value: newValue,
|
||||
trackedValue: newValue,
|
||||
initialValue: newValue,
|
||||
choices: getDefaultChoices(param)
|
||||
}
|
||||
}
|
||||
</Select>
|
||||
}
|
||||
const initialValue = getDefaultValueForType(param);
|
||||
return {
|
||||
...param, error: false,
|
||||
value: initialValue,
|
||||
trackedValue: initialValue,
|
||||
initialValue: initialValue,
|
||||
choices: getDefaultChoices(param)
|
||||
}
|
||||
});
|
||||
//params.sort((a, b) => -b.description.localeCompare(a.description));
|
||||
params.sort(sortByUiPositionThenName);
|
||||
newConfig[1].parameters = params;
|
||||
newConfig[2] = data.payload_by_pk.payloadcommands.map( c => c.command.cmd);
|
||||
newConfig[3] = {
|
||||
payload_type: selectedPayloadType,
|
||||
}
|
||||
const c2 = data.payload_by_pk.c2profileparametersinstances.reduce( (prev, cur) => {
|
||||
let inst = {...cur, ...cur.c2profileparameter};
|
||||
if(inst.parameter_type === "Array" || inst.parameter_type === "ChooseMultiple" || inst.parameter_type === "TypedArray" || inst.parameter_type === "FileMultiple"){
|
||||
try{
|
||||
inst["value"] = JSON.parse(inst["value"]);
|
||||
}catch(error){
|
||||
inst["value"] = inst["value"];
|
||||
}
|
||||
try{
|
||||
inst["trackedValue"] = JSON.parse(inst["value"]);
|
||||
}catch(error){
|
||||
inst["trackedValue"] = inst["value"];
|
||||
}
|
||||
inst["initialValue"] = getDefaultValueForType(inst);
|
||||
inst["choices"] = getDefaultChoices(inst);
|
||||
} else if(inst.parameter_type === "Dictionary"){
|
||||
//
|
||||
let choices = getDefaultChoices(inst);
|
||||
let finalDict = JSON.parse(inst["value"]); // this is a dictionary instead of an array, so fix it back
|
||||
let finalDictKeys = Object.keys(finalDict);
|
||||
let finalArray = [];
|
||||
for(let i = 0; i < finalDictKeys.length; i++){
|
||||
let newDict = {
|
||||
name: finalDictKeys[i],
|
||||
value: finalDict[finalDictKeys[i]],
|
||||
default_show: true
|
||||
};
|
||||
for(let j = 0; j < choices.length; j++){
|
||||
if(choices[j].name === finalDictKeys[i]){
|
||||
newDict["default_value"] = choices[j]["default_value"]
|
||||
}
|
||||
}
|
||||
finalArray.push(newDict);
|
||||
}
|
||||
choices = choices.map(c => {return {...c, default_show: false}});
|
||||
let initialValue = getDefaultValueForType(inst);
|
||||
inst = {...inst, value: finalArray, choices: choices, trackedValue: finalArray, initialValue: initialValue, default_value: initialValue};
|
||||
} else if(inst.parameter_type === "File") {
|
||||
inst["choices"] = getDefaultChoices(inst);
|
||||
inst["trackedValue"] = {name: inst["value"], legacy: true};
|
||||
inst["initialValue"] = getDefaultValueForType(inst);
|
||||
} else {
|
||||
inst["choices"] = getDefaultChoices(inst);
|
||||
inst["trackedValue"] = inst["value"];
|
||||
inst["initialValue"] = getDefaultValueForType(inst);
|
||||
}
|
||||
for(let i = 0; i < prev.length; i++){
|
||||
if(prev[i].name === cur.c2profileparameter.c2profile.name && prev[i].count === cur.count){
|
||||
prev[i].c2profileparameters.push({
|
||||
...inst
|
||||
});
|
||||
return [...prev];
|
||||
}
|
||||
}
|
||||
return [...prev, {
|
||||
id: cur.c2profileparameter.c2profile.id,
|
||||
is_p2p: cur.c2profileparameter.c2profile.is_p2p,
|
||||
name: cur.c2profileparameter.c2profile.name,
|
||||
count: cur.count,
|
||||
selected_instance: "None",
|
||||
c2profileparameters: [{
|
||||
...inst
|
||||
}],
|
||||
c2profileparametersinstances: [...cur.c2profileparameter.c2profile.c2profileparametersinstances]
|
||||
}]
|
||||
}, []);
|
||||
//console.log("before getModifiedC2Params", c2);
|
||||
|
||||
const profiles = c2.map( (profile) => {
|
||||
const parameters = getModifiedC2Params(profile, profile.c2profileparameters, newConfig[1], true);
|
||||
//parameters.sort((a,b) => -b.description.localeCompare(a.description));
|
||||
parameters.sort(sortByUiPositionThenName);
|
||||
return {...profile, c2profileparameters: parameters, "selected_instance": "None"};
|
||||
});
|
||||
newConfig[3].c2 = profiles;
|
||||
|
||||
//console.log("newConfig", newConfig);
|
||||
props.setAllData(newConfig);
|
||||
})
|
||||
.catch((data) => console.log(data));
|
||||
|
||||
}
|
||||
const onStartFresh = () => {
|
||||
finished(true)
|
||||
}
|
||||
const onUpdatePayloadConfig = (payload) => {
|
||||
payloadConfigRef.current = payload
|
||||
}
|
||||
return (
|
||||
<div style={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column"
|
||||
}}>
|
||||
{/* Content area that can grow */}
|
||||
<div style={{
|
||||
flexGrow: 1,
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: 0 // Important for flex shrinking
|
||||
}}>
|
||||
{/* Top section - fixed height */}
|
||||
<div style={{
|
||||
display: "flex",
|
||||
flexShrink: 0 // Don't shrink this section
|
||||
}}>
|
||||
<div style={{width: "100%", margin: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px"}}>
|
||||
<Typography variant={"p"} style={{fontWeight: 600}}>
|
||||
1. Select Operating System
|
||||
</Typography>
|
||||
<Select
|
||||
value={os}
|
||||
style={{width: "100%"}}
|
||||
disabled={!props.first}
|
||||
onChange={onChangeOS}
|
||||
>
|
||||
{
|
||||
osOptions.map((opt) => (
|
||||
<MenuItem key={"step1" + opt} value={opt}>{opt}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
<Typography style={{fontWeight: 600}}>
|
||||
Compatible Payload Types
|
||||
</Typography>
|
||||
{payloadtypesPerOS[os]?.join(", ")}
|
||||
<Typography style={{fontWeight: 600}}>
|
||||
Compatible C2 Profiles
|
||||
</Typography>
|
||||
{C2PerOS[os]?.join(", ")}
|
||||
</div>
|
||||
<div style={{width: "100%", margin: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px"}}>
|
||||
<div style={{width: "100%", display: "flex", alignItems: "flex-start", marginBottom: "10px", flexDirection: "column"}}>
|
||||
<Typography style={{fontWeight: 600}} variant={"p"}>
|
||||
2. Select Payload Type
|
||||
</Typography>
|
||||
<Select
|
||||
style={{width: "100%"}}
|
||||
disabled={!props.first}
|
||||
value={selectedPayloadType}
|
||||
onChange={evt => setSelectedPayloadType(evt.target.value)}
|
||||
>
|
||||
{
|
||||
payloadtypesPerOS[os]?.map((opt) => (
|
||||
<MenuItem key={"step1" + opt} value={opt}>{opt}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
|
||||
</div>
|
||||
<div style={{display: "flex"}}>
|
||||
<MythicAgentSVGIcon payload_type={selectedPayloadType} style={{width: "80px", padding: "5px", objectFit: "unset"}} />
|
||||
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
|
||||
<b>Version: </b>{payloadtypeData[selectedPayloadType]?.semver}<br/>
|
||||
<b>Description: </b>{payloadtypeData[selectedPayloadType]?.note}<br/>
|
||||
<b>C2: </b>{payloadtypeData[selectedPayloadType]?.payloadtypec2profiles.map(c => c.c2profile.name).join(", ")}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom section - scrollable table area */}
|
||||
<div style={{
|
||||
margin: "5px",
|
||||
border: props.first ? "1px solid grey" : '',
|
||||
borderRadius: "5px",
|
||||
padding: "10px 5px 5px 10px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
minHeight: 0, // Important for flex shrinking
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
{props.first &&
|
||||
<div style={{flexGrow: 1, overflowY: "auto", position: "relative"}}>
|
||||
{openBackdrop &&
|
||||
<Backdrop open={openBackdrop} onClick={()=>{setOpenBackdrop(false);}} style={{zIndex: 2000, position: "absolute"}}>
|
||||
<CircularProgress color="inherit" disableShrink />
|
||||
</Backdrop>
|
||||
}
|
||||
<StartFromExistingPayloadOrStartFresh first={props.first}
|
||||
last={props.last}
|
||||
canceled={canceled}
|
||||
onSelectedPayload={onSelectedPayload}
|
||||
payloadOptions={payloadOptions}
|
||||
onStartFresh={onStartFresh}
|
||||
/>
|
||||
</div>
|
||||
|
||||
}
|
||||
{!props.first &&
|
||||
<ConfigureBuildParameters os={os} selectedPayloadType={selectedPayloadType}
|
||||
prevData={props.prevData}
|
||||
onUpdatePayloadConfig={onUpdatePayloadConfig} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CreatePayloadNavigationButtons first={props.first} last={props.last} canceled={canceled}
|
||||
finished={finished}/>
|
||||
<br/><br/>
|
||||
{/* Navigation buttons - always at bottom */}
|
||||
<div style={{flexShrink: 0}}>
|
||||
<CreatePayloadNavigationButtons
|
||||
first={props.first}
|
||||
last={props.last}
|
||||
canceled={canceled}
|
||||
finished={finished}
|
||||
/>
|
||||
<br/><br/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const StartFromExistingPayloadOrStartFresh = (
|
||||
{first, last, canceled, onSelectedPayload, payloadOptions, onStartFresh}
|
||||
) => {
|
||||
return (
|
||||
<>
|
||||
{/* Header section - fixed */}
|
||||
<div style={{flexShrink: 0}}>
|
||||
<Typography variant={"p"} style={{fontWeight: 600}}>
|
||||
3. Continue from Existing Payload or
|
||||
<Button size="small" color={"primary"} variant={"contained"} style={{marginLeft: "10px", color: "white", marginBottom: "5px"}}
|
||||
onClick={onStartFresh}
|
||||
startIcon={<AddCircleIcon color={"success"} style={{backgroundColor: "white", borderRadius: "10px"}}/>} >
|
||||
Start Fresh
|
||||
</Button>
|
||||
</Typography>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Scrollable table container */}
|
||||
<div style={{
|
||||
flexGrow: 1,
|
||||
overflow: "auto",
|
||||
minHeight: 0 // Important for flex shrinking
|
||||
}}>
|
||||
<PayloadSelect
|
||||
payloadOptions={payloadOptions}
|
||||
first={first}
|
||||
last={last}
|
||||
canceled={canceled}
|
||||
finished={onSelectedPayload}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export const ConfigureBuildParameters = (
|
||||
{os, onUpdatePayloadConfig, selectedPayloadType, prevData}
|
||||
) => {
|
||||
const [payloadTypeConfigPieces, setPayloadTypeConfigPieces] = React.useState({});
|
||||
const [payloadTypeParameters, setSelectedPayloadTypeParameters] = React.useState([]);
|
||||
const getPayloadTypeBuildParameters = useMythicLazyQuery(GetBuildParametersQuery, {fetchPolicy: "network-only"});
|
||||
const onChange = (name, value, error) => {
|
||||
const newParams = payloadTypeParameters.map( (param) => {
|
||||
if(param.name === name){
|
||||
return {...param, value, error}
|
||||
}
|
||||
return {...param};
|
||||
});
|
||||
setSelectedPayloadTypeParameters(newParams);
|
||||
onUpdatePayloadConfig({
|
||||
"payload_type": selectedPayloadType,
|
||||
"parameters": newParams,
|
||||
...payloadTypeConfigPieces,
|
||||
"os": os});
|
||||
}
|
||||
React.useEffect( () => {
|
||||
if(selectedPayloadType === ""){return}
|
||||
getPayloadTypeBuildParameters({variables: {payloadtype: selectedPayloadType}})
|
||||
.then(({data}) => {
|
||||
if(data.payloadtype.length === 0){
|
||||
return
|
||||
}
|
||||
let extraConfig = {
|
||||
"file_extension": data.payloadtype[0].file_extension,
|
||||
"agent_type": data.payloadtype[0].agent_type,
|
||||
"supports_dynamic_loading": data.payloadtype[0].supports_dynamic_loading,
|
||||
"description": data.payloadtype[0].note,
|
||||
"supports_multiple_c2_in_build": data.payloadtype[0].supports_multiple_c2_in_build,
|
||||
"supports_multiple_c2_instances_in_build": data.payloadtype[0].supports_multiple_c2_instances_in_build,
|
||||
"c2_parameter_deviations": data.payloadtype[0].c2_parameter_deviations,
|
||||
"payload": prevData?.payload,
|
||||
"payload_type_id": data.payloadtype[0].id,
|
||||
}
|
||||
setPayloadTypeConfigPieces(extraConfig);
|
||||
if (prevData) {
|
||||
const params = data.payloadtype[0].buildparameters.map((param) => {
|
||||
for (let p = 0; p < prevData.parameters.length; p++) {
|
||||
if (prevData.parameters[p]["name"] === param.name) {
|
||||
return {
|
||||
...param, error: false,
|
||||
value: prevData.parameters[p]["value"],
|
||||
trackedValue: prevData.parameters[p]["value"],
|
||||
initialValue: getDefaultValueForType(param),
|
||||
choices: getDefaultChoices(param)
|
||||
}
|
||||
}
|
||||
}
|
||||
const initialValue = getDefaultValueForType(param);
|
||||
return {
|
||||
...param, error: false,
|
||||
value: initialValue,
|
||||
trackedValue: initialValue,
|
||||
initialValue: initialValue,
|
||||
choices: getDefaultChoices(param)
|
||||
}
|
||||
});
|
||||
//params.sort((a, b) => -b.description.localeCompare(a.description));
|
||||
params.sort(sortByUiPositionThenName);
|
||||
setSelectedPayloadTypeParameters(params);
|
||||
onUpdatePayloadConfig({
|
||||
"payload_type": selectedPayloadType,
|
||||
"parameters": prevData.parameters,
|
||||
...extraConfig,
|
||||
"os": os
|
||||
});
|
||||
return;
|
||||
}
|
||||
const params = data.payloadtype[0].buildparameters.map((param) => {
|
||||
const initialValue = getDefaultValueForType(param);
|
||||
return {
|
||||
...param,
|
||||
error: false,
|
||||
default_value:initialValue,
|
||||
value: initialValue,
|
||||
trackedValue: initialValue,
|
||||
initialValue: initialValue,
|
||||
choices: getDefaultChoices(param)
|
||||
}
|
||||
});
|
||||
//params.sort((a,b) => -b.description.localeCompare(a.description));
|
||||
params.sort(sortByUiPositionThenName);
|
||||
setSelectedPayloadTypeParameters(params);
|
||||
onUpdatePayloadConfig({
|
||||
"payload_type": selectedPayloadType,
|
||||
"parameters": params,
|
||||
...extraConfig,
|
||||
"os": os});
|
||||
})
|
||||
.catch((data) => console.log(data));
|
||||
}, [selectedPayloadType, prevData?.payload]);
|
||||
return (
|
||||
<>
|
||||
{/* Header section - fixed */}
|
||||
<div style={{flexShrink: 0}}>
|
||||
<Typography variant={"p"} style={{fontWeight: 600}}>
|
||||
3.5. Configure Payload Build Parameters
|
||||
</Typography>
|
||||
</div>
|
||||
<div style={{
|
||||
flexGrow: 1,
|
||||
overflow: "auto",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
minHeight: 0 // Important for flex shrinking
|
||||
}}>
|
||||
<div style={{
|
||||
width: "30%",
|
||||
margin: "5px",
|
||||
border: "1px solid grey",
|
||||
borderRadius: "5px",
|
||||
padding: "10px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
minHeight: 0, // Important for flex shrinking
|
||||
overflow: "auto"
|
||||
}}>
|
||||
<Typography textAlign="center" variant={"h7"} style={{fontWeight: 600, width: "100%"}}>
|
||||
Configuration Summary
|
||||
</Typography>
|
||||
<ConfigurationSummary buildParameters={payloadTypeParameters} os={os} />
|
||||
</div>
|
||||
<div style={{
|
||||
width: "100%",
|
||||
margin: "5px",
|
||||
border: "1px solid grey",
|
||||
borderRadius: "5px",
|
||||
padding: "0px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
minHeight: 0, // Important for flex shrinking
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
<CreatePayloadBuildParametersTable onChange={onChange} buildParameters={payloadTypeParameters} os={os}
|
||||
payload_type={selectedPayloadType}/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export const sortByUiPositionThenName = (a, b) => {
|
||||
if(a.ui_position === b.ui_position){
|
||||
return -b.name.localeCompare(a.name);
|
||||
}else if(a.ui_position < b.ui_position){
|
||||
return -1;
|
||||
}else if(a.ui_position > b.ui_position){
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
const HideConditionOperandEQ = "eq"
|
||||
const HideConditionOperandNotEQ = "neq"
|
||||
const HideConditionOperandIN = "in"
|
||||
const HideConditionOperandNotIN = "nin"
|
||||
const HideConditionOperandLessThan = "lt"
|
||||
const HideConditionOperandGreaterThan = "gt"
|
||||
const HideConditionOperandLessThanOrEqual = "lte"
|
||||
const HideConditionOperandGreaterThanOrEqual = "gte"
|
||||
const HideConditionOperationStartsWith = "sw"
|
||||
const HideConditionOperationEndsWith = "ew"
|
||||
const HideConditionOperationContains = "co"
|
||||
const HideConditionOperationNotContains = "nco"
|
||||
export const GetGroupedParameters = ({buildParameters, os, c2_name}) => {
|
||||
let groups = buildParameters?.reduce( (prev, cur) => {
|
||||
if(prev.includes(cur?.group_name)){return [...prev]}
|
||||
return [...prev, cur.group_name];
|
||||
}, []);
|
||||
let groupedData = groups.map(g => {
|
||||
return {name: g, parameters: []}
|
||||
});
|
||||
if(c2_name){
|
||||
buildParameters.sort(sortByUiPositionThenName);
|
||||
return [{name: c2_name, parameters: buildParameters}];
|
||||
}
|
||||
for(let i = 0; i < buildParameters.length; i++){
|
||||
for(let j = 0; j < groupedData.length; j++){
|
||||
if(buildParameters[i].group_name === groupedData[j].name){
|
||||
// only add the parameter if it doesn't meet a hide_condition
|
||||
let should_hide = false;
|
||||
if((buildParameters[i]?.supported_os?.length || 0) > 0){
|
||||
if(!buildParameters[i]?.supported_os?.includes(os)){
|
||||
should_hide = true;
|
||||
}
|
||||
}
|
||||
for(let k = 0; k < buildParameters[i]?.hide_conditions?.length || 0; k++){
|
||||
for(let l = 0; l < buildParameters.length; l++){
|
||||
if(buildParameters[l].name === buildParameters[i].hide_conditions[k].name){
|
||||
switch(buildParameters[i].hide_conditions[k].operand){
|
||||
case HideConditionOperandEQ:
|
||||
if(String(buildParameters[i].hide_conditions[k].value) === String(buildParameters[l].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
break;
|
||||
case HideConditionOperandNotEQ:
|
||||
if(String(buildParameters[i].hide_conditions[k].value) !== String(buildParameters[l].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
break;
|
||||
case HideConditionOperandIN:
|
||||
if(buildParameters[i].hide_conditions[k].choices.includes(buildParameters[l].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
break;
|
||||
case HideConditionOperandNotIN:
|
||||
if(!buildParameters[i].hide_conditions[k].choices.includes(buildParameters[l].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
break;
|
||||
case HideConditionOperandLessThan:
|
||||
try{
|
||||
if(parseInt(buildParameters[l].value) < parseInt(buildParameters[i].hide_conditions[k].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
}catch(e){
|
||||
console.log("couldn't parse build parameter value as int", e);
|
||||
}
|
||||
|
||||
break;
|
||||
case HideConditionOperandLessThanOrEqual:
|
||||
try{
|
||||
if(parseInt(buildParameters[l].value) <= parseInt(buildParameters[i].hide_conditions[k].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
}catch(e){
|
||||
console.log("couldn't parse build parameter value as int", e);
|
||||
}
|
||||
break;
|
||||
case HideConditionOperandGreaterThan:
|
||||
try{
|
||||
if(parseInt(buildParameters[l].value) > parseInt(buildParameters[i].hide_conditions[k].value) ){
|
||||
should_hide = true;
|
||||
}
|
||||
}catch(e){
|
||||
console.log("couldn't parse build parameter value as int", e);
|
||||
}
|
||||
break;
|
||||
case HideConditionOperandGreaterThanOrEqual:
|
||||
try{
|
||||
if(parseInt(buildParameters[l].value) >= parseInt(buildParameters[i].hide_conditions[k].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
}catch(e){
|
||||
console.log("couldn't parse build parameter value as int", e);
|
||||
}
|
||||
break;
|
||||
case HideConditionOperationStartsWith:
|
||||
if(String(buildParameters[l].value).startsWith(String(buildParameters[i].hide_conditions[k].value))){
|
||||
should_hide = true;
|
||||
}
|
||||
break;
|
||||
case HideConditionOperationEndsWith:
|
||||
if(String(buildParameters[l].value).endsWith(String(buildParameters[i].hide_conditions[k].value))){
|
||||
should_hide = true;
|
||||
}
|
||||
break;
|
||||
case HideConditionOperationContains:
|
||||
if(buildParameters[l].value.includes(buildParameters[i].hide_conditions[k].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
break;
|
||||
case HideConditionOperationNotContains:
|
||||
if(!buildParameters[l].value.includes(buildParameters[i].hide_conditions[k].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(should_hide){
|
||||
break;
|
||||
}
|
||||
groupedData[j].parameters.push(buildParameters[i]);
|
||||
groupedData[j].parameters.sort(sortByUiPositionThenName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
groupedData.sort(sortByUiPositionThenName)
|
||||
//groupedData.sort((a,b) => -b.name.localeCompare(a.name));
|
||||
return groupedData;
|
||||
}
|
||||
export const ConfigurationSummary = ({buildParameters, os, c2_name}) => {
|
||||
const theme = useTheme();
|
||||
const [groupedParameters, setGroupedParameters] = React.useState([]);
|
||||
React.useEffect( () => {
|
||||
// grouped should be array of groupName
|
||||
setGroupedParameters(GetGroupedParameters({buildParameters, os, c2_name}));
|
||||
}, [buildParameters, c2_name]);
|
||||
return (
|
||||
groupedParameters?.map((b,i) => (
|
||||
<div key={b.name} >
|
||||
{b.name !== '' && b.name !== undefined && b.parameters.length > 0 &&
|
||||
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", }} variant={"elevation"}>
|
||||
<Typography variant="h6" style={{textAlign: "left", display: "inline-block", marginLeft: "20px", color: theme.pageHeaderColor}}>
|
||||
{b.name}
|
||||
</Typography>
|
||||
</Paper>
|
||||
}
|
||||
{b?.parameters?.map( (p,i) => (
|
||||
<div key={p.name} style={{marginLeft: b.name === '' ? '' : "20px"}} className={i%2 > 0 ? 'alternateRow' : ''}>
|
||||
<Typography style={{fontWeight: 600}} variant={"body2"}>
|
||||
{p.name}
|
||||
</Typography>
|
||||
<div style={{marginLeft: "20px", marginTop: "5px", marginBottom: "5px", whiteSpace: "pre"}}>
|
||||
<ParseForDisplay cmd={p} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
)
|
||||
}
|
||||
@@ -8,29 +8,16 @@ import Typography from '@mui/material/Typography';
|
||||
import * as RandExp from 'randexp';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
|
||||
const GET_Payload_Types = gql`
|
||||
query getPayloadTypesBuildParametersQuery($os: jsonb!) {
|
||||
payloadtype(where: {supported_os: {_contains: $os}, deleted: {_eq: false}, wrapper: {_eq: false}, _or: [ {agent_type: {_eq: "agent"}}, {agent_type: {_eq: "service"}}] }, order_by: {name: asc}) {
|
||||
name
|
||||
id
|
||||
file_extension
|
||||
agent_type
|
||||
supports_dynamic_loading
|
||||
buildparameters(where: {deleted: {_eq: false} }, order_by: {description: asc}) {
|
||||
default_value
|
||||
description
|
||||
format_string
|
||||
id
|
||||
name
|
||||
parameter_type
|
||||
randomize
|
||||
required
|
||||
verifier_regex
|
||||
choices
|
||||
export const getSavedToType = (parameter) => {
|
||||
let temp = {...parameter};
|
||||
temp.default_value = parameter.trackedValue;
|
||||
if(parameter.parameter_type === 'Date'){
|
||||
return temp.default_value;
|
||||
} else if(parameter.parameter_type === 'File'){
|
||||
return {name: temp.trackedValue, legacy: true}
|
||||
}
|
||||
}
|
||||
return getDefaultValueForType(temp);
|
||||
}
|
||||
`;
|
||||
export const getDefaultValueForType = (parameter) => {
|
||||
// all default values will be strings, so convert them
|
||||
if(parameter.randomize && parameter.format_string !== ""){
|
||||
@@ -70,7 +57,7 @@ export const getDefaultValueForType = (parameter) => {
|
||||
case "FileMultiple":
|
||||
return [];
|
||||
case "File":
|
||||
return {name: ""};
|
||||
return {name: parameter.default_value};
|
||||
case "Date":
|
||||
// date default_value is a string of a number representing the day offset
|
||||
var tmpDate = new Date();
|
||||
@@ -86,149 +73,5 @@ export const getDefaultChoices = (parameter) => {
|
||||
} else {
|
||||
return parameter.choices;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function Step2SelectPayloadType(props){
|
||||
const [selectedPayloadType, setSelectedPayloadType] = React.useState('');
|
||||
const [fileExtension, setFileExtension] = React.useState('');
|
||||
const [supportsDynamicLoading, setSupportsDynamicLoading] = React.useState(false);
|
||||
const [agentType, setAgentType] = React.useState("agent");
|
||||
const [payloadTypeParameters, setSelectedPayloadTypeParameters] = React.useState([]);
|
||||
const { loading, error, data } = useQuery(GET_Payload_Types,
|
||||
{variables:{os: props.buildOptions },
|
||||
fetchPolicy: "no-cache",
|
||||
onCompleted: data => {
|
||||
if(data.payloadtype.length > 0){
|
||||
if(props.prevData !== undefined && props.prevData.os === props.buildOptions){
|
||||
//console.log("prevdata", props.prevData);
|
||||
setSelectedPayloadType(props.prevData.payload_type);
|
||||
setFileExtension(props.prevData.file_extension);
|
||||
setAgentType(props.prevData.agent_type);
|
||||
setSupportsDynamicLoading(props.prevData.supports_dynamic_loading);
|
||||
const payloadtypedata = data.payloadtype.reduce( (prev, payload) => {
|
||||
if(payload.name === props.prevData.payload_type){
|
||||
const params = payload.buildparameters.map( (param) => {
|
||||
for(let p = 0; p < props.prevData.parameters.length; p++){
|
||||
if(props.prevData.parameters[p]["name"] === param.name){
|
||||
return {...param, error: false,
|
||||
value: props.prevData.parameters[p]["value"],
|
||||
trackedValue: props.prevData.parameters[p]["value"],
|
||||
initialValue: getDefaultValueForType(param),
|
||||
choices: getDefaultChoices(param)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return [...prev, ...params];
|
||||
}
|
||||
return [...prev];
|
||||
}, []);
|
||||
payloadtypedata.sort((a,b) => -b.description.localeCompare(a.description));
|
||||
setSelectedPayloadTypeParameters(payloadtypedata);
|
||||
|
||||
}else{
|
||||
setSelectedPayloadType(data.payloadtype[0].name);
|
||||
setFileExtension(data.payloadtype[0].file_extension);
|
||||
setAgentType(data.payloadtype[0].agent_type);
|
||||
setSupportsDynamicLoading(data.payloadtype[0].supports_dynamic_loading);
|
||||
const payloadtypedata = data.payloadtype.reduce( (prev, payloadtype) => {
|
||||
if(payloadtype.name === data.payloadtype[0].name){
|
||||
const params = payloadtype.buildparameters.map( (param) => {
|
||||
const initialValue = getDefaultValueForType(param);
|
||||
return {...param, error: false,
|
||||
value: initialValue,
|
||||
trackedValue: initialValue,
|
||||
initialValue: initialValue,
|
||||
choices: getDefaultChoices(param)}
|
||||
});
|
||||
return [...prev, ...params];
|
||||
}
|
||||
return [...prev];
|
||||
}, []);
|
||||
payloadtypedata.sort((a,b) => -b.description.localeCompare(a.description));
|
||||
setSelectedPayloadTypeParameters(payloadtypedata);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const finished = () => {
|
||||
/*
|
||||
const finishedParams = payloadTypeParameters.map( (param) => {
|
||||
return {"name": param.name, "value": param.value}
|
||||
});
|
||||
*/
|
||||
props.finished({"payload_type": selectedPayloadType,
|
||||
"parameters": payloadTypeParameters,
|
||||
"file_extension": fileExtension,
|
||||
"supports_dynamic_loading": supportsDynamicLoading,
|
||||
"agent_type": agentType,
|
||||
"os": props.buildOptions});
|
||||
}
|
||||
const canceled = () => {
|
||||
props.canceled();
|
||||
}
|
||||
const changePayloadType = (evt) => {
|
||||
setSelectedPayloadType(evt.target.value);
|
||||
const payloadtypedata = data.payloadtype.reduce( (prev, payload) => {
|
||||
if(payload.name === evt.target.value){
|
||||
setFileExtension(payload.file_extension);
|
||||
setSupportsDynamicLoading(payload.supports_dynamic_loading);
|
||||
setAgentType(payload.agent_type);
|
||||
const params = payload.buildparameters.map( (param) => {
|
||||
const initialValue = getDefaultValueForType(param);
|
||||
return {...param, error: false,
|
||||
value: initialValue,
|
||||
trackedValue: initialValue,
|
||||
initialValue: initialValue,
|
||||
choices: getDefaultChoices(param)}
|
||||
});
|
||||
return [...prev, ...params];
|
||||
}
|
||||
return [...prev];
|
||||
}, []);
|
||||
payloadtypedata.sort((a,b) => -b.description.localeCompare(a.description));
|
||||
setSelectedPayloadTypeParameters(payloadtypedata);
|
||||
}
|
||||
const onChange = (name, value, error) => {
|
||||
const newParams = payloadTypeParameters.map( (param) => {
|
||||
if(param.name === name){
|
||||
return {...param, value, error}
|
||||
}
|
||||
return {...param};
|
||||
});
|
||||
setSelectedPayloadTypeParameters(newParams);
|
||||
}
|
||||
if (loading) {
|
||||
return <div><CircularProgress /></div>;
|
||||
}
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return <div>Error! {error.message}</div>;
|
||||
}
|
||||
return (
|
||||
<div style={{height: "100%", display: "flex", flexDirection: "column"}}>
|
||||
<Typography variant="h3" align="left" id="selectospage" component="div"
|
||||
style={{"marginLeft": "10px"}}>
|
||||
Select Target Payload Type
|
||||
</Typography>
|
||||
<Select
|
||||
value={selectedPayloadType}
|
||||
onChange={changePayloadType}
|
||||
>
|
||||
{
|
||||
data.payloadtype.map((opt) => (
|
||||
<MenuItem key={"step2" + opt.name} value={opt.name}>{opt.name}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select><br/>
|
||||
<div style={{display: "flex", flexGrow: 1, overflowY: "auto"}}>
|
||||
<CreatePayloadBuildParametersTable onChange={onChange} buildParameters={payloadTypeParameters} />
|
||||
</div>
|
||||
<CreatePayloadNavigationButtons first={props.first} last={props.last} canceled={canceled} finished={finished} />
|
||||
<br/><br/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,16 +3,18 @@ import {useQuery, gql} from '@apollo/client';
|
||||
import { CreatePayloadNavigationButtons} from './CreatePayloadNavigationButtons';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { MythicConfirmDialog } from '../../MythicComponents/MythicConfirmDialog';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import List from '@mui/material/List';
|
||||
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 Paper from '@mui/material/Paper';
|
||||
import Button from '@mui/material/Button';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import MenuBookIcon from '@mui/icons-material/MenuBook';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import {classes, StyledButton, StyledDivider} from '../../MythicComponents/MythicTransferList';
|
||||
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
|
||||
import MythicTextField from "../../MythicComponents/MythicTextField";
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
|
||||
const GET_Payload_Types = gql`
|
||||
query getCommands($payloadType: String!) {
|
||||
@@ -30,7 +32,9 @@ query getCommands($payloadType: String!) {
|
||||
|
||||
export function Step3SelectCommands(props){
|
||||
const confirmDialogCommands = React.useRef([]);
|
||||
const [hoveredCommand, setHoveredCommand] = React.useState({});
|
||||
const [commandOptions, setCommandOptions] = React.useState([]);
|
||||
const selectedCommands = React.useRef([]);
|
||||
const [openConfirmDialog, setOpenConfirmDialog] = React.useState(false);
|
||||
useQuery(GET_Payload_Types, {fetchPolicy: "network-only", variables: {payloadType: props.buildOptions["payload_type"]},
|
||||
onCompleted: ( data ) => {
|
||||
@@ -39,7 +43,8 @@ export function Step3SelectCommands(props){
|
||||
return {...c, selected: true, disabled: true, reason: "Always included because agent doesn't support dynamic loading"}
|
||||
});
|
||||
setCommandOptions(allCommands);
|
||||
}else{
|
||||
return
|
||||
}
|
||||
const allCommands = data.command.reduce( (prev, cur) => {
|
||||
try{
|
||||
const attributes = cur.attributes;
|
||||
@@ -49,7 +54,9 @@ export function Step3SelectCommands(props){
|
||||
}else{
|
||||
try{
|
||||
if(attributes["load_only"] !== undefined && attributes["load_only"]){
|
||||
return [...prev, {...cur, disabled: true, selected: false, reason: "This command can only be loaded in once a callback is established"}]
|
||||
// command can't be loaded here, don't bother showing it
|
||||
// {...cur, disabled: true, selected: false, reason: "This command can only be loaded in once a callback is established"}
|
||||
return [...prev]
|
||||
}
|
||||
let include_command = true;
|
||||
let build_option_that_sets_include_to_false = {};
|
||||
@@ -74,14 +81,18 @@ export function Step3SelectCommands(props){
|
||||
return [...prev, {...cur, disabled: false, selected: false, reason: ""}];
|
||||
}
|
||||
}else{
|
||||
return [...prev, {...cur, disabled: true, selected: false, reason: "Not available when build option \"" + build_option_that_sets_include_to_false["name"] + "\" is not \"" + build_option_that_sets_include_to_false["value"] + "\""}]
|
||||
// command not possible due to build option, don't show it
|
||||
// {...cur, disabled: true, selected: false, reason: "Not available when build option \"" + build_option_that_sets_include_to_false["name"] + "\" is not \"" + build_option_that_sets_include_to_false["value"] + "\""}
|
||||
return [...prev]
|
||||
}
|
||||
}catch(error){
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...prev, {...cur, disabled: true, selected: false, reason: "This command isn't supported by the selected OS"}];
|
||||
// command isn't supported by the OS, so don't even show it as an option
|
||||
// , {...cur, disabled: true, selected: false, reason: "This command isn't supported by the selected OS"}
|
||||
return [...prev];
|
||||
}catch(error){
|
||||
console.log(error);
|
||||
return [...prev, {...cur, disabled: false, selected: false, reason: "Failed to parse command attributes"}];
|
||||
@@ -99,25 +110,24 @@ export function Step3SelectCommands(props){
|
||||
setCommandOptions(selectedCommands);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const finished = (selectedCommands) => {
|
||||
const finished = () => {
|
||||
let foundExit = false;
|
||||
for(let i = 0; i < selectedCommands.length; i++){
|
||||
if(selectedCommands[i]["supported_ui_features"].includes("callback_table:exit")){
|
||||
for(let i = 0; i < selectedCommands.current.length; i++){
|
||||
if(selectedCommands.current[i]["supported_ui_features"].includes("callback_table:exit")){
|
||||
foundExit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(foundExit){
|
||||
const cmdNames = selectedCommands.map( c => c.cmd);
|
||||
const cmdNames = selectedCommands.current.map( c => c.cmd);
|
||||
props.finished(cmdNames);
|
||||
}else if(props.buildOptions["agent_type"] === "agent") {
|
||||
// only alert for agent types, not service types
|
||||
confirmDialogCommands.current = selectedCommands;
|
||||
confirmDialogCommands.current = selectedCommands.current;
|
||||
setOpenConfirmDialog(true);
|
||||
}else{
|
||||
const cmdNames = selectedCommands.map( c => c.cmd);
|
||||
const cmdNames = selectedCommands.current.map( c => c.cmd);
|
||||
props.finished(cmdNames);
|
||||
}
|
||||
}
|
||||
@@ -128,10 +138,149 @@ export function Step3SelectCommands(props){
|
||||
const canceled = () => {
|
||||
props.canceled();
|
||||
}
|
||||
const updateSelectedCommands = (commands) => {
|
||||
selectedCommands.current = commands;
|
||||
}
|
||||
return (
|
||||
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%", marginTop: "20px"}}>
|
||||
<CommandTransferSelect commands={commandOptions} payload_type={props.buildOptions["payload_type"]} first={props.first} last={props.last}
|
||||
canceled={canceled} finished={finished}/>
|
||||
<div style={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}>
|
||||
{/* Content area that can grow */}
|
||||
<div style={{
|
||||
flexGrow: 1,
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
minHeight: 0 // Important for flex shrinking
|
||||
}}>
|
||||
{/* Left section - fixed height */}
|
||||
<div style={{
|
||||
padding: "5px",
|
||||
display: "flex",
|
||||
width: "60%",
|
||||
flexDirection: "column",
|
||||
minHeight: 0,
|
||||
overflow: "hidden" // Don't shrink this section
|
||||
}}>
|
||||
<div style={{width: "100%", marginBottom: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px", display: "flex", flexShrink: 0}}>
|
||||
<MythicAgentSVGIcon payload_type={props.buildOptions.payload_type} style={{width: "80px", padding: "5px", objectFit: "unset"}} />
|
||||
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
|
||||
<b>Description: </b>{props.buildOptions.description}
|
||||
</Typography>
|
||||
</div>
|
||||
<div style={{width: "100%",
|
||||
//border: "1px solid grey",
|
||||
flexDirection: "column",
|
||||
borderRadius: "5px",
|
||||
//padding: "10px",
|
||||
flexGrow: 1, minHeight: 0, display: "flex"}}>
|
||||
<Typography variant={"p"} style={{fontWeight: 600}}>
|
||||
{"1. Select Commands to Include in the Payload"}
|
||||
</Typography>
|
||||
<div style={{flexGrow: 1, minHeight: 0, overflow: "hidden", display: "flex"}}>
|
||||
<CommandTransferSelect commands={commandOptions}
|
||||
payload_type={props.buildOptions["payload_type"]}
|
||||
first={props.first} last={props.last}
|
||||
updateSelectedCommands={updateSelectedCommands}
|
||||
setHoveredCommand={setHoveredCommand}/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom section - scrollable table area */}
|
||||
<div style={{
|
||||
width: "40%",
|
||||
padding: "5px",
|
||||
borderRadius: "5px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
minHeight: 0, // Important for flex shrinking
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
<div style={{width: "100%", flexGrow: 1, marginBottom: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px",
|
||||
height: "40%"}}>
|
||||
<Typography variant={"p"} style={{fontWeight: 600}}>
|
||||
{"Hovered Command Details"}
|
||||
</Typography><br/>
|
||||
{hoveredCommand["cmd"] !== undefined &&
|
||||
<>
|
||||
{hoveredCommand["cmd"]}
|
||||
<StyledDivider classes={{root: classes.divider}}/>
|
||||
<Typography style={{}}>
|
||||
<b>Description: </b>
|
||||
</Typography>
|
||||
<Typography style={{marginLeft: "20px"}}>
|
||||
{hoveredCommand["description"]}
|
||||
</Typography><br/>
|
||||
</>
|
||||
}
|
||||
|
||||
</div>
|
||||
<div style={{width: "100%", flexGrow: 1, marginBottom: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px",
|
||||
height: "20%", overflow: "scroll"}}>
|
||||
{hoveredCommand["reason"] !== "" ? (
|
||||
<>
|
||||
<Typography style={{}}>
|
||||
<b>{hoveredCommand["disabled"] ? ("Cannot be moved: ") : ("Information: ")} </b>
|
||||
</Typography>
|
||||
<Typography style={{marginLeft: "20px"}}>
|
||||
{hoveredCommand["reason"]}
|
||||
</Typography>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Typography style={{}}>
|
||||
<b>{"Information: "} </b>
|
||||
</Typography>
|
||||
<Typography style={{marginLeft: "20px"}}>
|
||||
{"This command can be included or removed as needed"}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
<Typography style={{}}>
|
||||
<b>Command line Help: </b>
|
||||
</Typography>
|
||||
<Typography style={{marginLeft: "20px"}}>
|
||||
{hoveredCommand["help_cmd"]}
|
||||
</Typography>
|
||||
<Typography style={{}}>
|
||||
<b>Needs Admin Permissions: </b>
|
||||
</Typography>
|
||||
<Typography style={{marginLeft: "20px"}}>
|
||||
{hoveredCommand["needs_admin"] ? "True" : "False"}
|
||||
</Typography>
|
||||
<Typography style={{}}>
|
||||
<b>Supported UI Features: </b>
|
||||
</Typography>
|
||||
<Typography style={{marginLeft: "20px"}}>
|
||||
{hoveredCommand?.supported_ui_features?.join(", ")}
|
||||
</Typography>
|
||||
{hoveredCommand?.attributes?.dependencies && hoveredCommand?.attributes?.dependencies.length > 0 &&
|
||||
<>
|
||||
<Typography style={{}}>
|
||||
<b>Dependencies: </b>
|
||||
</Typography>
|
||||
<Typography style={{marginLeft: "20px"}}>
|
||||
{hoveredCommand?.attributes?.dependencies.join(", ")}
|
||||
</Typography>
|
||||
</>}
|
||||
{hoveredCommand?.attributes?.alias !== undefined &&
|
||||
<>
|
||||
<Typography style={{}}>
|
||||
<b>Alias: </b>
|
||||
</Typography>
|
||||
<Typography style={{marginLeft: "20px"}}>
|
||||
{hoveredCommand?.attributes?.alias ? "True":"False"}
|
||||
</Typography>
|
||||
</>}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{openConfirmDialog &&
|
||||
<MythicConfirmDialog open={openConfirmDialog}
|
||||
title={"No exit command selected, continue?"}
|
||||
@@ -139,141 +288,137 @@ export function Step3SelectCommands(props){
|
||||
acceptText="Accept"
|
||||
onSubmit={acceptConfirm} />
|
||||
}
|
||||
{/* Navigation buttons - always at bottom */}
|
||||
<div style={{flexShrink: 0}}>
|
||||
<CreatePayloadNavigationButtons
|
||||
first={props.first}
|
||||
last={props.last}
|
||||
canceled={canceled}
|
||||
finished={finished}
|
||||
/>
|
||||
<br/><br/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandTransferSelect(props) {
|
||||
|
||||
const [checked, setChecked] = React.useState([]);
|
||||
const [left, setLeft] = React.useState([]);
|
||||
const [right, setRight] = React.useState([]);
|
||||
const leftChecked = intersection(checked, left);
|
||||
const rightChecked = intersection(checked, right);
|
||||
const [hoveredCommand, setHoveredCommnad] = React.useState({});
|
||||
function not(a, b) {
|
||||
return a.filter( (value) => b.find( (element) => element["cmd"] === value["cmd"] && !element["disabled"] ) === undefined)
|
||||
}
|
||||
|
||||
function intersection(a, b) {
|
||||
return a.filter( (value) => b.find( (element) => element["cmd"] === value["cmd"] && !element["disabled"] ) !== undefined)
|
||||
}
|
||||
const [commands, setCommands] = React.useState([]);
|
||||
const [leftFilter, setLeftFilter] = React.useState("");
|
||||
const [rightFilter, setRightFilter] = React.useState("");
|
||||
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);
|
||||
}
|
||||
|
||||
setChecked(newChecked);
|
||||
if(value.disabled){
|
||||
return;
|
||||
}
|
||||
const newCommands = commands.map( c => {
|
||||
if(c.cmd === value.cmd){
|
||||
return {...c, selected: !c.selected};
|
||||
}
|
||||
return {...c};
|
||||
});
|
||||
setCommands(newCommands);
|
||||
};
|
||||
|
||||
const handleAllRight = () => {
|
||||
const initialLeft = props.commands.reduce( (prev, cur) => {
|
||||
if(cur.disabled && !cur.selected){
|
||||
return [...prev, {...cur}];
|
||||
}else{
|
||||
return [...prev];
|
||||
}
|
||||
}, [])
|
||||
const initialRight = props.commands.reduce( (prev, cur) => {
|
||||
if(!(cur.disabled && !cur.selected)){
|
||||
return [...prev, {...cur}];
|
||||
}else{
|
||||
return [...prev];
|
||||
}
|
||||
}, [])
|
||||
setLeft(initialLeft);
|
||||
setRight(initialRight);
|
||||
const newCommands = commands.map( c => {
|
||||
if(c.disabled){return {...c}}
|
||||
return {...c, left: false, right: true};
|
||||
});
|
||||
setCommands(newCommands);
|
||||
};
|
||||
|
||||
const handleCheckedRight = () => {
|
||||
setRight(right.concat(leftChecked));
|
||||
setLeft(not(left, leftChecked));
|
||||
setChecked(not(checked, leftChecked));
|
||||
const newCommands = commands.map( c => {
|
||||
if(c.selected && c.left){
|
||||
return {...c, selected: false, left: false, right: true};
|
||||
}
|
||||
return {...c}
|
||||
});
|
||||
setCommands(newCommands);
|
||||
};
|
||||
|
||||
const handleCheckedLeft = () => {
|
||||
setLeft(left.concat(rightChecked));
|
||||
setRight(not(right, rightChecked));
|
||||
setChecked(not(checked, rightChecked));
|
||||
const newCommands = commands.map( c => {
|
||||
if(c.selected && c.right){
|
||||
return {...c, selected: false, left: true, right: false};
|
||||
}
|
||||
return {...c}
|
||||
});
|
||||
setCommands(newCommands);
|
||||
};
|
||||
|
||||
const handleAllLeft = () => {
|
||||
const initialLeft = props.commands.reduce( (prev, cur) => {
|
||||
if(!(cur.disabled && cur.selected)){
|
||||
return [...prev, {...cur}];
|
||||
}else{
|
||||
return [...prev];
|
||||
}
|
||||
}, [])
|
||||
const initialRight = props.commands.reduce( (prev, cur) => {
|
||||
if((cur.disabled && cur.selected)){
|
||||
return [...prev, {...cur}];
|
||||
}else{
|
||||
return [...prev];
|
||||
}
|
||||
}, [])
|
||||
setLeft(initialLeft);
|
||||
setRight(initialRight);
|
||||
const newCommands = commands.map( c => {
|
||||
if(c.disabled){return {...c}}
|
||||
return {...c, left: true, right: false};
|
||||
});
|
||||
setCommands(newCommands);
|
||||
};
|
||||
useEffect( () => {
|
||||
const initialLeft = props.commands.reduce( (prev, cur) => {
|
||||
if(!cur.selected){
|
||||
return [...prev, {...cur}];
|
||||
}else{
|
||||
return [...prev];
|
||||
}
|
||||
}, [])
|
||||
const initialRight = props.commands.reduce( (prev, cur) => {
|
||||
if(cur.selected){
|
||||
return [...prev, {...cur}];
|
||||
}else{
|
||||
return [...prev];
|
||||
}
|
||||
}, [])
|
||||
setLeft(initialLeft);
|
||||
setRight(initialRight);
|
||||
const newCommands = props.commands.map( c => {
|
||||
if(c.selected){
|
||||
return {...c, selected: false, left: false, right: true};
|
||||
}
|
||||
return {...c, selected: false, left: true, right: false};
|
||||
});
|
||||
setCommands(newCommands);
|
||||
}, [props.commands]);
|
||||
useEffect( () => {
|
||||
props.updateSelectedCommands(commands.filter(c => c.right));
|
||||
}, [commands]);
|
||||
const setHoveredData = (event) => {
|
||||
const cmd = props.commands.filter( c => c.cmd === event.target.innerText );
|
||||
if(cmd.length > 0){
|
||||
setHoveredCommnad(cmd[0]);
|
||||
props.setHoveredCommand(cmd[0]);
|
||||
}
|
||||
}
|
||||
const customList = (title, items) => (
|
||||
<Paper style={{width:"100%", height: "calc(40vh)", display: "flex", flexDirection: "column", overflowY: "auto"}} elevation={5}>
|
||||
<CardHeader title={title} />
|
||||
<StyledDivider className={classes.divider}/>
|
||||
<div style={{display: "flex", flexGrow: 1, overflowY: "auto", width: "100%"}}>
|
||||
<List dense component="div" role="list" style={{padding:0, width: "100%", overflow: "auto", backgroundColor: "unset"}}>
|
||||
const updateLeftFilter = (name, value, error) => {
|
||||
setLeftFilter(value);
|
||||
}
|
||||
const updateRightFilter = (name, value, error) => {
|
||||
setRightFilter(value);
|
||||
}
|
||||
const customList = (title, items, filter, setFilter) => (
|
||||
<div style={{width:"100%", flexGrow: 1, display: "flex", flexDirection: "column", minHeight: 0, overflow: "hidden", padding: "5px", }} >
|
||||
<Typography style={{fontWeight: 600}} variant={"h5"}>
|
||||
{title}
|
||||
</Typography>
|
||||
<MythicTextField value={filter} onChange={setFilter} name={"Search Commands"} width={"100%"}
|
||||
marginBottom={"0px"}
|
||||
InputProps={{startAdornment: (
|
||||
<InputAdornment position={"start"}>
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
)}}
|
||||
/>
|
||||
<StyledDivider className={classes.divider}/>
|
||||
<div style={{display: "flex", flexGrow: 1, width: "100%", overflow: "hidden", minHeight: 0, }}>
|
||||
<List dense component="div" role="list" style={{
|
||||
flexGrow: 1, // Subtract header height
|
||||
padding:0, width: "100%", overflow: "auto", backgroundColor: "unset", maxHeight: "100%"}}>
|
||||
{items.map((valueObj) => {
|
||||
const value = valueObj["cmd"];
|
||||
const labelId = `transfer-list-item-${value}-label`;
|
||||
if(filter !== "" && !value.includes(filter.toLowerCase())){return null}
|
||||
return (
|
||||
<div onMouseEnter={setHoveredData} key={'commandtransfer' + value}>
|
||||
<ListItem style={{padding:0}} disabled={valueObj["disabled"]}
|
||||
key={value} role="listitem" button onClick={handleToggle(valueObj)}
|
||||
<ListItem style={{padding:0}} disabled={valueObj.disabled}
|
||||
key={value} role="listitem" onClick={handleToggle(valueObj)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Checkbox
|
||||
disabled={valueObj["disabled"]}
|
||||
checked={checked.findIndex( (element) => element["cmd"] === value) !== -1}
|
||||
disabled={valueObj.disabled}
|
||||
checked={valueObj.selected}
|
||||
tabIndex={-1}
|
||||
disableRipple
|
||||
inputProps={{ 'aria-labelledby': labelId }}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText id={labelId} primary={value} />
|
||||
<ListItemText id={labelId} primary={
|
||||
<div style={{display: "flex", alignItems: "center", justifyContent: "space-between"}}>
|
||||
{value}
|
||||
<IconButton variant="contained" onClick={(e) => e.stopPropagation()}
|
||||
href={"/docs/agents/" + props.payload_type + "/commands/" + value}
|
||||
style={{marginLeft: "10px", float: "right"}} target="_blank">
|
||||
<MenuBookIcon/>
|
||||
</IconButton>
|
||||
</div>
|
||||
} />
|
||||
</ListItem>
|
||||
</div>
|
||||
);
|
||||
@@ -281,26 +426,18 @@ function CommandTransferSelect(props) {
|
||||
<ListItem />
|
||||
</List>
|
||||
</div>
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
const finished = () => {
|
||||
props.finished(right);
|
||||
}
|
||||
return (
|
||||
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%"}}>
|
||||
<div style={{flexGrow: 1}}>
|
||||
<Grid container spacing={2} justifyContent="center" alignItems="center" className={classes.root}>
|
||||
<Grid size={5}>
|
||||
{customList("Available Commands", left)}
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Grid container direction="column" alignItems="center">
|
||||
return (
|
||||
<div style={{flexGrow: 1, width: "100%", display: "flex", minHeight: 0, alignItems: "stretch"}} className={classes.root}>
|
||||
{customList("Commands Available", commands.filter(c => c.left), leftFilter, updateLeftFilter)}
|
||||
<div style={{display: "flex", flexDirection: "column", flexShrink: 0, justifyContent: "center"}}>
|
||||
<StyledButton
|
||||
variant="contained"
|
||||
size="small"
|
||||
className={classes.button}
|
||||
onClick={handleAllRight}
|
||||
disabled={left.length === 0}
|
||||
disabled={commands.filter(c => c.left && !c.disabled).length === 0}
|
||||
aria-label="move all right"
|
||||
>
|
||||
>>
|
||||
@@ -310,7 +447,7 @@ return (
|
||||
size="small"
|
||||
className={classes.button}
|
||||
onClick={handleCheckedRight}
|
||||
disabled={leftChecked.length === 0}
|
||||
disabled={commands.filter(c => c.left && c.selected).length === 0}
|
||||
aria-label="move selected right"
|
||||
>
|
||||
>
|
||||
@@ -320,7 +457,7 @@ return (
|
||||
size="small"
|
||||
className={classes.button}
|
||||
onClick={handleCheckedLeft}
|
||||
disabled={rightChecked.length === 0}
|
||||
disabled={commands.filter( c => c.right && c.selected).length === 0}
|
||||
aria-label="move selected left"
|
||||
>
|
||||
<
|
||||
@@ -330,60 +467,21 @@ return (
|
||||
size="small"
|
||||
className={classes.button}
|
||||
onClick={handleAllLeft}
|
||||
disabled={right.length === 0}
|
||||
disabled={commands.filter(c => c.right && !c.disabled).length === 0}
|
||||
aria-label="move all left"
|
||||
>
|
||||
<<
|
||||
</StyledButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid size={5}>
|
||||
{customList("Commands Included", right)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container justifyContent="center" alignItems="flex-start" className={classes.root}>
|
||||
<Grid style={{height: "100%", marginBottom: "10px"}} size={12}>
|
||||
{hoveredCommand["cmd"] !== undefined &&
|
||||
<Paper className={classes.paper} style={{width: "100%"}} elevation={5}>
|
||||
</div>
|
||||
{customList("Commands in Payload", commands.filter(c => c.right), rightFilter, updateRightFilter)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<CardHeader
|
||||
title={
|
||||
<React.Fragment>
|
||||
{hoveredCommand["cmd"]}
|
||||
<Button variant="contained" color="primary"
|
||||
href={"/docs/agents/" + props.payload_type + "/commands/" + hoveredCommand["cmd"]}
|
||||
style={{marginLeft: "10px", float: "right"}} target="_blank">Documentation
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
<StyledDivider classes={{root: classes.divider}}/>
|
||||
{hoveredCommand["reason"] !== "" ? (
|
||||
<Typography variant="body1" align="left" component="div"
|
||||
style={{"marginLeft": "10px"}}><b>{hoveredCommand["disabled"] ? ("Disabled Reason: ") : ("Information: ")} </b>{hoveredCommand["reason"]}
|
||||
</Typography>
|
||||
) : null}
|
||||
<br/>
|
||||
<Typography align="left" component="div"
|
||||
style={{"marginLeft": "10px"}}><b>Commandline
|
||||
Help: </b>{hoveredCommand["help_cmd"]}
|
||||
</Typography>
|
||||
<Typography align="left" component="div"
|
||||
style={{"marginLeft": "10px"}}><b>Needs Admin
|
||||
Permissions: </b>{hoveredCommand["needs_admin"] ? "True" : "False"}
|
||||
</Typography>
|
||||
<Typography align="left" component="div"
|
||||
style={{"marginLeft": "10px"}}><b>Description: </b>{hoveredCommand["description"]}
|
||||
</Typography>
|
||||
</Paper>
|
||||
}
|
||||
/*
|
||||
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%", marginTop: "20px"}}>
|
||||
<CommandTransferSelect commands={commandOptions} payload_type={props.buildOptions["payload_type"]} first={props.first} last={props.last}
|
||||
canceled={canceled} finished={finished}/>
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
<CreatePayloadNavigationButtons first={props.first} last={props.last} canceled={props.canceled}
|
||||
finished={finished}/>
|
||||
<br/><br/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
*/
|
||||
@@ -1,23 +1,24 @@
|
||||
import React from 'react';
|
||||
import {useQuery, gql, useLazyQuery} from '@apollo/client';
|
||||
import {snackActions} from '../../utilities/Snackbar';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import { CreatePayloadNavigationButtons} from './CreatePayloadNavigationButtons';
|
||||
import {CreatePayloadC2ProfileParametersTable} from './CreatePayloadC2ProfileParametersTable';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import { meState } from '../../../cache';
|
||||
import {useReactiveVar} from '@apollo/client';
|
||||
import Select from '@mui/material/Select';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import {getDefaultValueForType, getDefaultChoices} from './Step2SelectPayloadType';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import MythicStyledTableCell from '../../MythicComponents/MythicTableCell';
|
||||
import AddCircleIcon from '@mui/icons-material/AddCircle';
|
||||
import Button from '@mui/material/Button';
|
||||
import {MythicConfirmDialog} from "../../MythicComponents/MythicConfirmDialog";
|
||||
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
|
||||
import {CreatePayloadBuildParametersTable} from "./CreatePayloadBuildParametersTable";
|
||||
import Tab from '@mui/material/Tab';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import { IconButton } from '@mui/material';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery";
|
||||
import {ConfigurationSummary} from "./Step1SelectOS";
|
||||
|
||||
|
||||
const GET_Payload_Types = gql`
|
||||
@@ -38,6 +39,7 @@ query getPayloadTypesC2ProfilesQuery($payloadType: String!, $operation_id: Int!)
|
||||
required
|
||||
verifier_regex
|
||||
choices
|
||||
ui_position
|
||||
}
|
||||
c2profileparametersinstances(where: {instance_name: {_is_null: false}, operation_id: {_eq: $operation_id}}, distinct_on: instance_name, order_by: {instance_name: asc}){
|
||||
instance_name
|
||||
@@ -60,6 +62,7 @@ query getProfileInstanceQuery($name: String!, $operation_id: Int!, $c2_profile_i
|
||||
required
|
||||
verifier_regex
|
||||
choices
|
||||
ui_position
|
||||
c2profile {
|
||||
name
|
||||
}
|
||||
@@ -85,100 +88,143 @@ query getDefaultC2ProfileParameters($c2profile_id: Int!) {
|
||||
required
|
||||
verifier_regex
|
||||
choices
|
||||
ui_position
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const getModifiedC2Params = (c2, c2profileparameters, buildOptions, use_supplied_values) => {
|
||||
return c2profileparameters.reduce( (prev, param) => {
|
||||
const initialValue = getDefaultValueForType(param);
|
||||
let configuredParam = {...param,
|
||||
error: false,
|
||||
value: initialValue,
|
||||
trackedValue: initialValue,
|
||||
initialValue: initialValue,
|
||||
choices: getDefaultChoices(param)}
|
||||
if(use_supplied_values){
|
||||
configuredParam = {...param}
|
||||
}
|
||||
if(buildOptions.c2_parameter_deviations === null){
|
||||
return [...prev, {...configuredParam}];
|
||||
}
|
||||
if(buildOptions.c2_parameter_deviations[c2.name] === undefined){
|
||||
return [...prev, {...configuredParam}];
|
||||
}
|
||||
if(buildOptions.c2_parameter_deviations[c2.name][param.name] === undefined){
|
||||
return [...prev, {...configuredParam}];
|
||||
}
|
||||
let c2Config = buildOptions.c2_parameter_deviations[c2.name][param.name];
|
||||
if(c2Config.supported === false){
|
||||
return [...prev];
|
||||
}
|
||||
if(c2Config.default_value !== undefined){
|
||||
configuredParam.value = c2Config.default_value;
|
||||
configuredParam.trackedValue = c2Config.default_value;
|
||||
configuredParam.initialValue = c2Config.default_value;
|
||||
}
|
||||
if(c2Config.dictionary_choices !== undefined && c2Config.dictionary_choices !== null){
|
||||
configuredParam.choices = c2Config.dictionary_choices.map(c => {return {...c, value: c.default_value}});
|
||||
configuredParam.value = configuredParam.choices;
|
||||
configuredParam.trackedValue = configuredParam.choices;
|
||||
configuredParam.initialValue = configuredParam.choices;
|
||||
}else if(c2Config.choices !== undefined && c2Config.choices !== null){
|
||||
configuredParam.choices = c2Config.choices;
|
||||
if(!configuredParam.choices.includes(configuredParam.default_value)){
|
||||
if(configuredParam.choices.length > 0){
|
||||
configuredParam.value = configuredParam.choices[0];
|
||||
configuredParam.trackedValue = configuredParam.choices[0];
|
||||
configuredParam.initialValue = configuredParam.choices[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...prev, {...configuredParam}];
|
||||
}, []);
|
||||
}
|
||||
export function Step4C2Profiles(props){
|
||||
const me = useReactiveVar(meState);
|
||||
const [disabledC2Add, setDisabledC2Add] = React.useState(false);
|
||||
const [openConfirmDialog, setOpenConfirmDialog] = React.useState(false);
|
||||
const [selectedC2, setSelectedC2] = React.useState("None");
|
||||
const [includedC2Profiles, setIncludedC2Profiles] = React.useState([]);
|
||||
const [c2Profiles, setC2Profiles] = React.useState([]);
|
||||
const { loading, error } = useQuery(GET_Payload_Types, {variables:{payloadType: props.buildOptions["payload_type"], operation_id: me?.user?.current_operation_id || 0},
|
||||
useQuery(GET_Payload_Types, {variables:{payloadType: props.buildOptions["payload_type"], operation_id: me?.user?.current_operation_id || 0},
|
||||
onCompleted: data => {
|
||||
const profiles = data.c2profile.map( (c2) => {
|
||||
if(props.prevData !== undefined){
|
||||
//console.log(props.prevData);
|
||||
for(let p = 0; p < props.prevData.length; p++){
|
||||
if(props.prevData[p]["name"] === c2.name){
|
||||
// we selected this c2 profile before and clicked back, so re-fill it out
|
||||
const parameters = props.prevData[p]["c2profileparameters"].map( (param) => {
|
||||
return {...param, error: false,
|
||||
value: param["value"],
|
||||
trackedValue: param["value"],
|
||||
initialValue: getDefaultValueForType(param),
|
||||
choices: getDefaultChoices(param)
|
||||
}
|
||||
})
|
||||
|
||||
parameters.sort((a,b) => -b.description.localeCompare(a.description));
|
||||
return {...c2, "selected": props.prevData[p]["selected"], c2profileparameters: parameters, "selected_instance": props.prevData[p]["selected_instance"]};
|
||||
}
|
||||
}
|
||||
const parameters = getModifiedC2Params(c2, c2.c2profileparameters, props.buildOptions, false);
|
||||
let hasUiPositions = parameters.filter(p => p.ui_position > 0).length > 0;
|
||||
if(hasUiPositions){
|
||||
parameters.sort((a,b) => a.ui_position > b.ui_position ? 1 : a.ui_position === b.ui_position ? 0 : -1);
|
||||
} else {
|
||||
parameters.sort((a,b) => -b.description.localeCompare(a.description));
|
||||
}
|
||||
const parameters = c2.c2profileparameters.map( (param) => {
|
||||
const initialValue = getDefaultValueForType(param);
|
||||
return {...param, error: false, value: initialValue,
|
||||
trackedValue: initialValue,
|
||||
initialValue: initialValue,
|
||||
choices: getDefaultChoices(param)};
|
||||
|
||||
});
|
||||
parameters.sort((a,b) => -b.description.localeCompare(a.description));
|
||||
return {...c2, "selected": false, c2profileparameters: parameters, "selected_instance": "None"};
|
||||
|
||||
return {...c2, c2profileparameters: parameters, "selected_instance": "None"};
|
||||
});
|
||||
profiles.sort((a, b) => -b.name.localeCompare(a.name))
|
||||
//console.log(profiles);
|
||||
if(profiles.length > 0){
|
||||
setSelectedC2(profiles[0]);
|
||||
}
|
||||
setC2Profiles(profiles);
|
||||
if(props.prevData !== undefined){
|
||||
// if we changed payload type, blow away the c2 configs
|
||||
if(props.prevData.payload_type === props.buildOptions.payload_type) {
|
||||
const goodPrevData = props.prevData.c2.reduce( (prev, cur) => {
|
||||
// only keep prevData that matches up with the current available c2 profiles for this payload type
|
||||
for(let i = 0; i < profiles.length; i++){
|
||||
if(cur.name === profiles[i].name){
|
||||
cur.c2profileparameters = getModifiedC2Params(cur, cur.c2profileparameters, props.buildOptions, true);
|
||||
return [...prev, {...cur}];
|
||||
}
|
||||
}
|
||||
return [...prev];
|
||||
}, []);
|
||||
setIncludedC2Profiles(goodPrevData);
|
||||
}
|
||||
}
|
||||
},
|
||||
fetchPolicy: "no-cache"
|
||||
});
|
||||
const acceptConfirm = () => {
|
||||
props.finished(c2Profiles);
|
||||
props.finished(includedC2Profiles);
|
||||
}
|
||||
const finished = () => {
|
||||
let allValid = true;
|
||||
let includedC2 = false;
|
||||
c2Profiles.forEach( (c2) => {
|
||||
if(c2.selected){
|
||||
includedC2 = true;
|
||||
c2.c2profileparameters.forEach( (param) => {
|
||||
if(param.error){
|
||||
snackActions.warning(c2.name + "'s parameter " + param.name + " is invalid");
|
||||
allValid = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
includedC2Profiles.forEach( (c2) => {
|
||||
includedC2 = true;
|
||||
c2.c2profileparameters.forEach( (param) => {
|
||||
if(param.error){
|
||||
snackActions.warning(c2.name + "'s parameter " + param.name + " is invalid");
|
||||
allValid = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
if(allValid){
|
||||
//console.log(c2Profiles);
|
||||
//console.log(includedC2Profiles);
|
||||
if(!includedC2 && props.buildOptions["agent_type"] === "agent"){
|
||||
// normal agents need to confirm they're not including c2, services can move through
|
||||
setOpenConfirmDialog(true);
|
||||
return;
|
||||
}
|
||||
props.finished(c2Profiles);
|
||||
props.finished({
|
||||
"c2": includedC2Profiles,
|
||||
"payload_type": props.buildOptions.payload_type
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
const canceled = () => {
|
||||
props.canceled();
|
||||
}
|
||||
const toggleC2Selection = (evt, c2, selected) => {
|
||||
const updatedc2 = c2Profiles.map( (curc2) => {
|
||||
if(c2.name === curc2.name){
|
||||
return {...curc2, selected: selected}
|
||||
}
|
||||
return curc2;
|
||||
});
|
||||
setC2Profiles(updatedc2);
|
||||
}
|
||||
const updateC2Parameter = (c2Name, parameterName, value, error) => {
|
||||
const updatedc2 = c2Profiles.map( (curC2) => {
|
||||
if(curC2.name === c2Name){
|
||||
const updateC2Parameter = (index, parameterName, value, error) => {
|
||||
const updatedc2 = includedC2Profiles.map( (curC2, indx) => {
|
||||
if(indx === index){
|
||||
const c2params = curC2.c2profileparameters.map( (param) => {
|
||||
if (param.name === parameterName){
|
||||
return {...param, error, value}
|
||||
return {...param, error, value, trackedValue: value}
|
||||
}
|
||||
return {...param};
|
||||
});
|
||||
@@ -186,132 +232,303 @@ export function Step4C2Profiles(props){
|
||||
}
|
||||
return curC2;
|
||||
});
|
||||
setC2Profiles(updatedc2);
|
||||
setIncludedC2Profiles(updatedc2);
|
||||
}
|
||||
const [getInstanceValues] = useLazyQuery(getProfileInstanceQuery, {
|
||||
onCompleted: (data) => {
|
||||
const updates = data.c2profileparametersinstance.map( (cur) => {
|
||||
let inst = {...cur, ...cur.c2profileparameter};
|
||||
if(inst.parameter_type === "Array" || inst.parameter_type === "ChooseMultiple" || inst.parameter_type === "TypedArray" || inst.parameter_type === "FileMultiple"){
|
||||
try{
|
||||
inst["value"] = JSON.parse(inst["value"]);
|
||||
}catch(error){
|
||||
inst["value"] = inst["value"];
|
||||
}
|
||||
try{
|
||||
inst["trackedValue"] = JSON.parse(inst["value"]);
|
||||
}catch(error){
|
||||
inst["trackedValue"] = inst["value"];
|
||||
}
|
||||
inst["initialValue"] = getDefaultValueForType(inst);
|
||||
inst["choices"] = getDefaultChoices(inst);
|
||||
} else if(inst.parameter_type === "Dictionary"){
|
||||
//
|
||||
let choices = getDefaultChoices(inst);
|
||||
let finalDict = JSON.parse(inst["value"]); // this is a dictionary instead of an array, so fix it back
|
||||
let finalDictKeys = Object.keys(finalDict);
|
||||
let finalArray = [];
|
||||
for(let i = 0; i < finalDictKeys.length; i++){
|
||||
let newDict = {
|
||||
name: finalDictKeys[i],
|
||||
value: finalDict[finalDictKeys[i]],
|
||||
default_show: true
|
||||
};
|
||||
for(let j = 0; j < choices.length; j++){
|
||||
if(choices[j].name === finalDictKeys[i]){
|
||||
newDict["default_value"] = choices[j]["default_value"]
|
||||
}
|
||||
}
|
||||
finalArray.push(newDict);
|
||||
}
|
||||
|
||||
choices = choices.map(c => {return {...c, default_show: false}});
|
||||
let initialValue = getDefaultValueForType(inst);
|
||||
return {...inst, value: finalArray, choices: choices, trackedValue: finalArray, initialValue: initialValue, default_value: initialValue};
|
||||
} else if(inst.parameter_type === "File") {
|
||||
inst["choices"] = getDefaultChoices(inst);
|
||||
inst["trackedValue"] = {name: inst["value"], legacy: true};
|
||||
inst["initialValue"] = getDefaultValueForType(inst);
|
||||
} else {
|
||||
inst["choices"] = getDefaultChoices(inst);
|
||||
inst["trackedValue"] = inst["value"];
|
||||
inst["initialValue"] = getDefaultValueForType(inst);
|
||||
}
|
||||
return inst;
|
||||
})
|
||||
updates.sort( (a, b) => a.description < b.description ? -1 : 1);
|
||||
const updatedc2 = c2Profiles.map( (curc2) => {
|
||||
if(updates[0].c2profile.name === curc2.name){
|
||||
return {...curc2, c2profileparameters: updates};
|
||||
}
|
||||
return curc2;
|
||||
});
|
||||
setC2Profiles(updatedc2);
|
||||
},
|
||||
onError: (data) => {
|
||||
snackActions.error("Failed to fetch instance data: " + data);
|
||||
console.log(data);
|
||||
},
|
||||
const getInstanceValues = useMythicLazyQuery(getProfileInstanceQuery, {
|
||||
fetchPolicy: "no-cache"
|
||||
});
|
||||
const [getIDefaultValues] = useLazyQuery(getDefaultsQuery, {
|
||||
onCompleted: (data) => {
|
||||
const updates = data.c2profile_by_pk.c2profileparameters.map( (param) => {
|
||||
const initialValue = getDefaultValueForType(param);
|
||||
return {...param, error: false, value: initialValue,
|
||||
trackedValue: initialValue,
|
||||
initialValue: initialValue,
|
||||
choices: getDefaultChoices(param)};
|
||||
})
|
||||
updates.sort( (a, b) => a.description < b.description ? -1 : 1);
|
||||
const updatedc2 = c2Profiles.map( (curc2) => {
|
||||
if(data.c2profile_by_pk.name === curc2.name){
|
||||
return {...curc2, c2profileparameters: updates};
|
||||
}
|
||||
return curc2;
|
||||
});
|
||||
setC2Profiles(updatedc2);
|
||||
},
|
||||
onError: (data) => {
|
||||
snackActions.error("Failed to fetch instance data: " + data);
|
||||
console.log(data);
|
||||
},
|
||||
const getIDefaultValues = useMythicLazyQuery(getDefaultsQuery, {
|
||||
fetchPolicy: "no-cache"
|
||||
});
|
||||
const onChangeCreatedInstanceName = (evt, c2) => {
|
||||
c2.selected_instance = evt.target.value;
|
||||
//setSelectedInstance(evt.target.value);
|
||||
const updatedc2 = c2Profiles.map( (curc2) => {
|
||||
if(c2.name === curc2.name){
|
||||
curc2.selected = true;
|
||||
curc2.c2profileparameters = [];
|
||||
}
|
||||
return curc2;
|
||||
});
|
||||
setC2Profiles(updatedc2);
|
||||
const onChangeCreatedInstanceName = (evt, index, c2) => {
|
||||
if(evt.target.value !== "None"){
|
||||
getInstanceValues({variables: {name: evt.target.value, operation_id: me?.user?.current_operation_id || 0, c2_profile_id: c2.id}});
|
||||
getInstanceValues({variables: {name: evt.target.value, operation_id: me?.user?.current_operation_id || 0, c2_profile_id: c2.id}})
|
||||
.then(({data}) => {
|
||||
let updates = data.c2profileparametersinstance.map( (cur) => {
|
||||
let inst = {...cur, ...cur.c2profileparameter};
|
||||
if(inst.parameter_type === "Array" || inst.parameter_type === "ChooseMultiple" || inst.parameter_type === "TypedArray" || inst.parameter_type === "FileMultiple"){
|
||||
try{
|
||||
inst["value"] = JSON.parse(inst["value"]);
|
||||
}catch(error){
|
||||
inst["value"] = inst["value"];
|
||||
}
|
||||
try{
|
||||
inst["trackedValue"] = JSON.parse(inst["value"]);
|
||||
}catch(error){
|
||||
inst["trackedValue"] = inst["value"];
|
||||
}
|
||||
inst["initialValue"] = getDefaultValueForType(inst);
|
||||
inst["choices"] = getDefaultChoices(inst);
|
||||
} else if(inst.parameter_type === "Dictionary"){
|
||||
//
|
||||
let choices = getDefaultChoices(inst);
|
||||
let finalDict = JSON.parse(inst["value"]); // this is a dictionary instead of an array, so fix it back
|
||||
let finalDictKeys = Object.keys(finalDict);
|
||||
let finalArray = [];
|
||||
for(let i = 0; i < finalDictKeys.length; i++){
|
||||
let newDict = {
|
||||
name: finalDictKeys[i],
|
||||
value: finalDict[finalDictKeys[i]],
|
||||
default_show: true
|
||||
};
|
||||
for(let j = 0; j < choices.length; j++){
|
||||
if(choices[j].name === finalDictKeys[i]){
|
||||
newDict["default_value"] = choices[j]["default_value"]
|
||||
}
|
||||
}
|
||||
finalArray.push(newDict);
|
||||
}
|
||||
|
||||
choices = choices.map(c => {return {...c, default_show: false}});
|
||||
let initialValue = getDefaultValueForType(inst);
|
||||
inst = {...inst, value: finalArray, choices: choices, trackedValue: finalArray, initialValue: initialValue, default_value: initialValue};
|
||||
} else if(inst.parameter_type === "File") {
|
||||
inst["choices"] = getDefaultChoices(inst);
|
||||
inst["trackedValue"] = {name: inst["value"], legacy: true};
|
||||
inst["initialValue"] = getDefaultValueForType(inst);
|
||||
} else {
|
||||
inst["choices"] = getDefaultChoices(inst);
|
||||
inst["trackedValue"] = inst["value"];
|
||||
inst["initialValue"] = getDefaultValueForType(inst);
|
||||
}
|
||||
return inst;
|
||||
});
|
||||
updates = getModifiedC2Params(c2, updates, props.buildOptions, true);
|
||||
//updates.sort( (a, b) => a.description < b.description ? -1 : 1);
|
||||
let hasUiPositions = updates.filter(p => p.ui_position > 0).length > 0;
|
||||
if(hasUiPositions){
|
||||
updates.sort((a,b) => a.ui_position > b.ui_position ? 1 : a.ui_position === b.ui_position ? 0 : -1);
|
||||
} else {
|
||||
updates.sort((a,b) => -b.description.localeCompare(a.description));
|
||||
}
|
||||
const updatedc2 = includedC2Profiles.map( (curc2, indx) => {
|
||||
if(index === indx){
|
||||
return {...curc2, c2profileparameters: updates, selected_instance: evt.target.value};
|
||||
}
|
||||
return {...curc2};
|
||||
});
|
||||
setIncludedC2Profiles([...updatedc2]);
|
||||
})
|
||||
.catch((data) => {
|
||||
snackActions.error("Failed to fetch instance data: " + data);
|
||||
console.log("error fetching", data);
|
||||
});
|
||||
} else {
|
||||
getIDefaultValues({variables: {c2profile_id: c2.id}});
|
||||
getIDefaultValues({variables: {c2profile_id: c2.id}})
|
||||
.then(({data}) => {
|
||||
let updates = data.c2profile_by_pk.c2profileparameters.map( (param) => {
|
||||
const initialValue = getDefaultValueForType(param);
|
||||
return {...param, error: false,
|
||||
value: initialValue,
|
||||
trackedValue: initialValue,
|
||||
initialValue: initialValue,
|
||||
choices: getDefaultChoices(param)};
|
||||
});
|
||||
updates = getModifiedC2Params(c2, updates, props.buildOptions, false);
|
||||
let hasUiPositions = updates.filter(p => p.ui_position > 0).length > 0;
|
||||
if(hasUiPositions){
|
||||
updates.sort((a,b) => a.ui_position > b.ui_position ? 1 : a.ui_position === b.ui_position ? 0 : -1);
|
||||
} else {
|
||||
updates.sort((a,b) => -b.description.localeCompare(a.description));
|
||||
}
|
||||
//updates.sort( (a, b) => a.description < b.description ? -1 : 1);
|
||||
const updatedc2 = includedC2Profiles.map( (curc2, indx) => {
|
||||
if(index === indx){
|
||||
return {...curc2, c2profileparameters: updates, selected_instance: 'None'};
|
||||
}
|
||||
return {...curc2};
|
||||
});
|
||||
setIncludedC2Profiles(updatedc2);
|
||||
})
|
||||
.catch((data) => {
|
||||
snackActions.error("Failed to fetch instance data: " + data);
|
||||
console.log(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (loading) {
|
||||
return <div><CircularProgress /></div>;
|
||||
const onChangeSelectedC2 = (evt) => {
|
||||
setSelectedC2(evt.target.value);
|
||||
}
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return <div>Error! {error.message}</div>;
|
||||
const addC2 = () => {
|
||||
setIncludedC2Profiles([...includedC2Profiles, {...selectedC2}]);
|
||||
}
|
||||
const removeC2 = (i) => {
|
||||
setIncludedC2Profiles(includedC2Profiles.toSpliced(i, 1));
|
||||
}
|
||||
React.useEffect( () => {
|
||||
if(selectedC2 === 'None'){
|
||||
setDisabledC2Add(true);
|
||||
return;
|
||||
}
|
||||
if(!props.buildOptions.supports_multiple_c2_in_build && includedC2Profiles.length > 0){
|
||||
setDisabledC2Add(true);
|
||||
return;
|
||||
}
|
||||
if(!props.buildOptions.supports_multiple_c2_instances_in_build){
|
||||
const c2Names = includedC2Profiles.map(c => c.name);
|
||||
if(c2Names.includes(selectedC2.name)){
|
||||
setDisabledC2Add(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setDisabledC2Add(false);
|
||||
}, [includedC2Profiles, selectedC2]);
|
||||
return (
|
||||
<div style={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column"
|
||||
}}>
|
||||
{/* Content area that can grow */}
|
||||
<div style={{
|
||||
flexGrow: 1,
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: 0 // Important for flex shrinking
|
||||
}}>
|
||||
{/* Top section - fixed height */}
|
||||
<div style={{
|
||||
display: "flex",
|
||||
flexShrink: 0 // Don't shrink this section
|
||||
}}>
|
||||
<div style={{width: "40%", margin: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px", display: "flex"}}>
|
||||
<MythicAgentSVGIcon payload_type={props.buildOptions.payload_type} style={{width: "80px", padding: "5px", objectFit: "unset"}} />
|
||||
<div>
|
||||
<Typography variant={"p"} style={{}}>
|
||||
<b>OS: </b>{props.buildOptions.os}
|
||||
</Typography><br/>
|
||||
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
|
||||
<b>Description: </b>{props.buildOptions.description}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{width: "60%",
|
||||
margin: "5px",
|
||||
border: "1px solid grey",
|
||||
borderRadius: "5px", padding: "10px"}}>
|
||||
<Typography variant={"p"} style={{fontWeight: 600}}>
|
||||
1. Select C2 Profiles to Include
|
||||
</Typography>
|
||||
<Select
|
||||
style={{width: "100%", marginBottom: "5px", marginTop: "5px", display: "flex"}}
|
||||
value={selectedC2}
|
||||
onChange={onChangeSelectedC2}
|
||||
>
|
||||
<MenuItem key={"buildparamopt" + "-1"}
|
||||
value={"None"}>None</MenuItem>
|
||||
{
|
||||
c2Profiles.map((opt, i) => (
|
||||
<MenuItem key={"buildparamopt" + i} value={opt} style={{display: "flex"}}>
|
||||
<b>{opt.name}</b> - {opt.description}
|
||||
</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
<Button size="small" color={"primary"} variant={"contained"} style={{marginLeft: "10px", color: "white", marginBottom: "5px"}}
|
||||
onClick={addC2}
|
||||
disabled={disabledC2Add}
|
||||
startIcon={<AddCircleIcon color={"success"} style={{backgroundColor: "white", borderRadius: "10px"}}/>} >
|
||||
Include Profile
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom section - scrollable table area */}
|
||||
<div style={{
|
||||
margin: "5px",
|
||||
//border: "1px solid grey",
|
||||
borderRadius: "5px",
|
||||
//padding: "10px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
minHeight: 0, // Important for flex shrinking
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
{/* Header section - fixed */}
|
||||
<div style={{flexShrink: 0}}>
|
||||
<Typography variant={"p"} style={{fontWeight: 600}}>
|
||||
2. Include and Configure C2 Profiles
|
||||
</Typography>
|
||||
</div>
|
||||
<div style={{
|
||||
flexGrow: 1,
|
||||
overflow: "auto",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
minHeight: 0 // Important for flex shrinking
|
||||
}}>
|
||||
<div style={{
|
||||
width: "30%",
|
||||
margin: "5px",
|
||||
border: "1px solid grey",
|
||||
borderRadius: "5px",
|
||||
padding: "5px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
minHeight: 0, // Important for flex shrinking
|
||||
overflow: "auto"
|
||||
}}>
|
||||
<Typography textAlign="center" variant={"h7"} style={{fontWeight: 600, width: "100%"}}>
|
||||
Configuration Summary
|
||||
</Typography>
|
||||
{includedC2Profiles.map( (c, index) => (
|
||||
<ConfigurationSummary key={c.name + index} buildParameters={c.c2profileparameters}
|
||||
os={props.buildOptions.os} c2_name={c.name} />
|
||||
))}
|
||||
</div>
|
||||
<div style={{
|
||||
width: "100%",
|
||||
margin: "5px",
|
||||
border: "1px solid grey",
|
||||
borderRadius: "5px",
|
||||
padding: "0px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
minHeight: 0, // Important for flex shrinking
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
<C2ProfileTabs includedC2Profiles={includedC2Profiles} os={props.buildOptions.os}
|
||||
onCloseTab={removeC2} onChange={updateC2Parameter}
|
||||
onChangeCreatedInstanceName={onChangeCreatedInstanceName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{openConfirmDialog &&
|
||||
<MythicConfirmDialog open={openConfirmDialog}
|
||||
title={"No C2 Profiles selected, continue?"}
|
||||
onClose={() => setOpenConfirmDialog(false)}
|
||||
acceptText="Accept"
|
||||
onSubmit={acceptConfirm} />
|
||||
}
|
||||
{/* Navigation buttons - always at bottom */}
|
||||
<div style={{flexShrink: 0}}>
|
||||
<CreatePayloadNavigationButtons
|
||||
first={props.first}
|
||||
last={props.last}
|
||||
canceled={canceled}
|
||||
finished={finished}
|
||||
/>
|
||||
<br/><br/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
/*
|
||||
return (
|
||||
<div style={{paddingTop: "20px", display: "flex", flexDirection: "column", height: "100%", width: "100%"}}>
|
||||
<div style={{flexGrow: 1, width: "100%", overflowY: "scroll"}}>
|
||||
<Table stickyHeader={true} size="small" style={{"maxWidth": "100%",}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{width: "4rem"}}>Include?</TableCell>
|
||||
<TableCell>C2 Name</TableCell>
|
||||
<TableCell>Pre-created Instances</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
<MythicStyledTableCell style={{width: "4rem"}}>Include?</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>C2 Name</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>Pre-created Instances</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>Description</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
@@ -361,11 +578,11 @@ export function Step4C2Profiles(props){
|
||||
</TableRow>
|
||||
{c2.selected &&
|
||||
<TableRow>
|
||||
<MythicStyledTableCell colSpan={4}>
|
||||
<TableCell colSpan={4} style={{padding: "0px 0px 0px 0px !important", margin: "0px !important"}}>
|
||||
<CreatePayloadC2ProfileParametersTable key={"step4table" + c2.id}
|
||||
returnAllDictValues={false} {...c2}
|
||||
onChange={updateC2Parameter}/>
|
||||
</MythicStyledTableCell>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
}
|
||||
@@ -388,4 +605,90 @@ export function Step4C2Profiles(props){
|
||||
<br/><br/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
function a11yProps(index) {
|
||||
return {
|
||||
id: `simple-tab-${index}`,
|
||||
'aria-controls': `simple-tabpanel-${index}`,
|
||||
};
|
||||
}
|
||||
function CustomTabPanel(props) {
|
||||
const { children, value, index, ...other } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`simple-tabpanel-${index}`}
|
||||
aria-labelledby={`simple-tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
{value === index && children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const C2ProfileTabs = ({includedC2Profiles, onChange, os, onCloseTab, onChangeCreatedInstanceName}) => {
|
||||
const [value, setValue] = React.useState(0);
|
||||
const onCloseTabLocal = (e, index) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onCloseTab(index);
|
||||
setValue(0);
|
||||
}
|
||||
const handleChange = (event, newValue) => {
|
||||
setValue(newValue);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Tabs
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
indicatorColor='primary'
|
||||
textColor='primary'
|
||||
variant='fullWidth'
|
||||
scrollButtons={true}
|
||||
style={{minHeight: "40px", flexShrink: 0 }}
|
||||
aria-label='scrollable tabs'>
|
||||
{includedC2Profiles.map( (c, index) => (
|
||||
<Tab key={c.name + index} label={
|
||||
<div style={{display: "flex", alignItems: "center"}}>
|
||||
{c.name}
|
||||
<IconButton size='small' onClick={(e) => onCloseTabLocal(e, index)} >
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
} {...a11yProps(index)} style={{flexShrink: 0}} />
|
||||
))}
|
||||
</Tabs>
|
||||
{includedC2Profiles.map( (c, index) => (
|
||||
<CustomTabPanel index={index} value={value} key={c.name + index} style={{
|
||||
overflowY: "auto", display: "flex", flexDirection: "column",
|
||||
}} >
|
||||
{c.c2profileparametersinstances.length > 0 &&
|
||||
<Select
|
||||
style={{width: "100%", marginBottom: "5px"}}
|
||||
value={c.selected_instance}
|
||||
onChange={evt => onChangeCreatedInstanceName(evt, index, c)}
|
||||
>
|
||||
<MenuItem key={"buildparamopt" + "-1"}
|
||||
value={"None"}>None</MenuItem>
|
||||
{
|
||||
c.c2profileparametersinstances.map((opt, i) => (
|
||||
<MenuItem key={"buildparamopt" + i}
|
||||
value={opt.instance_name}>{opt.instance_name}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
}
|
||||
<CreatePayloadBuildParametersTable instance_name={c.selected_instance} onChange={(n,v,e) => onChange(index, n, v, e)}
|
||||
buildParameters={c.c2profileparameters} os={os}
|
||||
c2_name={c.name}
|
||||
/>
|
||||
</CustomTabPanel>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,12 @@ import MythicTextField from '../../MythicComponents/MythicTextField';
|
||||
import {snackActions} from '../../utilities/Snackbar';
|
||||
import {UploadTaskFile} from "../../MythicComponents/MythicFileUpload";
|
||||
import {getSkewedNow} from "../../utilities/Time";
|
||||
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
|
||||
import {ConfigurationSummary, GetGroupedParameters} from "./Step1SelectOS";
|
||||
import MenuBookIcon from '@mui/icons-material/MenuBook';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import DriveFileRenameOutlineIcon from '@mui/icons-material/DriveFileRenameOutline';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
|
||||
const create_payload = gql`
|
||||
mutation createPayloadMutation($payload: String!) {
|
||||
@@ -23,7 +29,6 @@ export function Step5Build(props){
|
||||
const [description, setDescription] = React.useState("");
|
||||
const [startSubscription, setStartSubscription] = React.useState(false);
|
||||
const [subscriptionID, setSubscriptionID] = React.useState("");
|
||||
const [showExtraOptions, setShowExtraOptions] = React.useState(false);
|
||||
const [createPayloadMutation] = useMutation(create_payload, {
|
||||
update: (cache, {data}) => {
|
||||
setSubscriptionID(data.createPayload.uuid);
|
||||
@@ -32,7 +37,6 @@ export function Step5Build(props){
|
||||
if(!startSubscription){
|
||||
setStartSubscription(true);
|
||||
}
|
||||
setShowExtraOptions(true);
|
||||
}else{
|
||||
snackActions.error(data.createPayload.error);
|
||||
}
|
||||
@@ -58,8 +62,14 @@ export function Step5Build(props){
|
||||
}
|
||||
const finished = async () => {
|
||||
let buildParameters = [];
|
||||
for(let i = 0; i < props.buildOptions[1]["parameters"].length; i++){
|
||||
let param = props.buildOptions[1]["parameters"][i];
|
||||
let params = GetGroupedParameters({
|
||||
buildParameters: props.buildOptions[1]["parameters"],
|
||||
os: props.buildOptions[1].os,
|
||||
c2_name: undefined}).reduce( (prev, cur) => {
|
||||
return [...prev, ...cur.parameters];
|
||||
}, []);
|
||||
for(let i = 0; i < params.length; i++){
|
||||
let param = params[i];
|
||||
if (param.parameter_type === "Dictionary") {
|
||||
const newDict = param.value.reduce((prev, cur) => {
|
||||
if (cur.default_show) {
|
||||
@@ -71,6 +81,8 @@ export function Step5Build(props){
|
||||
} else if (param.parameter_type === "File") {
|
||||
if (typeof param.value === "string") {
|
||||
buildParameters.push({name: param.name, value: param.value});
|
||||
}else if(param.value.legacy){
|
||||
buildParameters.push({name: param.name, value: param.value.name});
|
||||
} else {
|
||||
const newUUID = await UploadTaskFile(param.value, "Uploaded as build parameter for " + filename);
|
||||
if (newUUID) {
|
||||
@@ -87,6 +99,8 @@ export function Step5Build(props){
|
||||
for(let j = 0; j < param.value.length; j++){
|
||||
if (typeof param.value[j] === "string") {
|
||||
fileMultipleValues.push(param.value[j]);
|
||||
}else if(param.value[j].legacy){
|
||||
fileMultipleValues.push(param.value[j].name);
|
||||
} else {
|
||||
const newUUID = await UploadTaskFile(param.value[j], "Uploaded as build parameter for " + filename);
|
||||
if (newUUID) {
|
||||
@@ -105,27 +119,50 @@ export function Step5Build(props){
|
||||
}
|
||||
}
|
||||
let c2Profiles = [];
|
||||
for(let i = 0; i < props.buildOptions[3].length; i++){
|
||||
if(props.buildOptions[3][i].selected){
|
||||
let instanceParam = {};
|
||||
for(let j = 0; j < props.buildOptions[3][i].c2profileparameters.length; j++){
|
||||
let param = props.buildOptions[3][i].c2profileparameters[j];
|
||||
if(param.parameter_type === "Dictionary"){
|
||||
const newDict = param.value.reduce( (prev, cur) => {
|
||||
if(cur.default_show){
|
||||
return {...prev, [cur.name]: cur.value};
|
||||
for(let i = 0; i < props.buildOptions[3]?.c2?.length; i++){
|
||||
let instanceParam = {};
|
||||
let c2params = GetGroupedParameters({
|
||||
buildParameters: props.buildOptions[3].c2[i].c2profileparameters,
|
||||
os: props.buildOptions[1].os,
|
||||
c2_name: props.buildOptions[3].c2[i].name}).reduce( (prev, cur) => {
|
||||
return [...prev, ...cur.parameters];
|
||||
}, []);
|
||||
for(let j = 0; j < c2params.length; j++){
|
||||
let param = c2params[j];
|
||||
if(param.parameter_type === "Dictionary"){
|
||||
const newDict = param.value.reduce( (prev, cur) => {
|
||||
if(cur.default_show){
|
||||
return {...prev, [cur.name]: cur.value};
|
||||
}
|
||||
return {...prev}
|
||||
}, {});
|
||||
instanceParam = {...instanceParam, [param.name]: newDict};
|
||||
} else if (param.parameter_type === "File") {
|
||||
if (typeof param.value === "string") {
|
||||
instanceParam = {...instanceParam, [param.name]: param.value};
|
||||
} else {
|
||||
const newUUID = await UploadTaskFile(param.value, "Uploaded as c2 parameter for " + filename);
|
||||
if (newUUID) {
|
||||
if (newUUID !== "Missing file in form") {
|
||||
instanceParam = {...instanceParam, [param.name]: newUUID};
|
||||
} else {
|
||||
snackActions.error("Failed to upload files, missing file")
|
||||
}
|
||||
return {...prev}
|
||||
}, {});
|
||||
instanceParam = {...instanceParam, [param.name]: newDict};
|
||||
} else if (param.parameter_type === "File") {
|
||||
if (typeof param.value === "string") {
|
||||
instanceParam = {...instanceParam, [param.name]: param.value};
|
||||
} else {
|
||||
const newUUID = await UploadTaskFile(param.value, "Uploaded as c2 parameter for " + filename);
|
||||
snackActions.error("Failed to upload files")
|
||||
return;
|
||||
}
|
||||
}
|
||||
}else if(param.parameter_type === "FileMultiple"){
|
||||
let fileMultipleValues = [];
|
||||
for(let j = 0; j < param.value.length; j++){
|
||||
if (typeof param.value[j] === "string") {
|
||||
fileMultipleValues.push(param.value[j]);
|
||||
} else {
|
||||
const newUUID = await UploadTaskFile(param.value[j], "Uploaded as c2 parameter for " + filename);
|
||||
if (newUUID) {
|
||||
if (newUUID !== "Missing file in form") {
|
||||
instanceParam = {...instanceParam, [param.name]: newUUID};
|
||||
fileMultipleValues.push(newUUID);
|
||||
} else {
|
||||
snackActions.error("Failed to upload files, missing file")
|
||||
}
|
||||
@@ -134,40 +171,22 @@ export function Step5Build(props){
|
||||
return;
|
||||
}
|
||||
}
|
||||
}else if(param.parameter_type === "FileMultiple"){
|
||||
let fileMultipleValues = [];
|
||||
for(let j = 0; j < param.value.length; j++){
|
||||
if (typeof param.value[j] === "string") {
|
||||
fileMultipleValues.push(param.value[j]);
|
||||
} else {
|
||||
const newUUID = await UploadTaskFile(param.value[j], "Uploaded as c2 parameter for " + filename);
|
||||
if (newUUID) {
|
||||
if (newUUID !== "Missing file in form") {
|
||||
fileMultipleValues.push(newUUID);
|
||||
} else {
|
||||
snackActions.error("Failed to upload files, missing file")
|
||||
}
|
||||
} else {
|
||||
snackActions.error("Failed to upload files")
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
instanceParam = {...instanceParam, [param.name]: fileMultipleValues};
|
||||
} else {
|
||||
instanceParam = {...instanceParam, [param.name]: param.value};
|
||||
//return {...prev, [param.name]: param.value}
|
||||
}
|
||||
instanceParam = {...instanceParam, [param.name]: fileMultipleValues};
|
||||
} else {
|
||||
instanceParam = {...instanceParam, [param.name]: param.value};
|
||||
//return {...prev, [param.name]: param.value}
|
||||
}
|
||||
c2Profiles.push({
|
||||
"c2_profile": props.buildOptions[3][i].name,
|
||||
"c2_profile_parameters": instanceParam,
|
||||
})
|
||||
}
|
||||
c2Profiles.push({
|
||||
"c2_profile": props.buildOptions[3].c2[i].name,
|
||||
"c2_profile_parameters": instanceParam,
|
||||
})
|
||||
|
||||
}
|
||||
const finishedPayload = {
|
||||
"selected_os": props.buildOptions[0],
|
||||
"payload_type": props.buildOptions[1]["payload_type"],
|
||||
"selected_os": props.buildOptions[1].os,
|
||||
"payload_type": props.buildOptions[1].payload_type,
|
||||
"filename": filename,
|
||||
"description": description,
|
||||
"commands": props.buildOptions[2],
|
||||
@@ -183,7 +202,164 @@ export function Step5Build(props){
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%"}}>
|
||||
<div style={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column"
|
||||
}}>
|
||||
{/* Content area that can grow */}
|
||||
<div style={{
|
||||
flexGrow: 1,
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: 0 // Important for flex shrinking
|
||||
}}>
|
||||
{/* Top section - fixed height */}
|
||||
<div style={{
|
||||
display: "flex",
|
||||
flexShrink: 0 // Don't shrink this section
|
||||
}}>
|
||||
<div style={{width: "100%", margin: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px", display: "flex"}}>
|
||||
<MythicAgentSVGIcon payload_type={props.buildOptions[1].payload_type} style={{width: "80px", padding: "5px", objectFit: "unset"}} />
|
||||
<div>
|
||||
<Typography variant={"p"} style={{}}>
|
||||
<b>OS: </b>{props.buildOptions[1].os}
|
||||
<MythicStyledTooltip title={"Edit OS / Payload Type"}>
|
||||
<IconButton color={"primary"} onClick={() => props.moveToStep(0)}>
|
||||
<DriveFileRenameOutlineIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</Typography><br/>
|
||||
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
|
||||
<b>Description: </b>{props.buildOptions[1].description}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{width: "100%", margin: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px"}}>
|
||||
<Typography variant={"p"} style={{fontWeight: 600}}>
|
||||
1. Provide Payload Name and Description
|
||||
</Typography>
|
||||
<div style={{width: "100%", display: "flex", alignItems: "flex-start", marginBottom: "10px", flexDirection: "column"}}>
|
||||
<MythicTextField onEnter={finished} autoFocus={true} required={false} placeholder={"Filename"}
|
||||
value={filename} multiline={false} onChange={onChangeFilename} display="inline-block"/>
|
||||
<MythicTextField onEnter={finished} required={false} placeholder={"description"} value={description}
|
||||
multiline={false} onChange={onChangeDescription} display="inline-block"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom section - scrollable table area */}
|
||||
<div style={{
|
||||
margin: "5px",
|
||||
// border: "1px solid grey",
|
||||
borderRadius: "5px",
|
||||
//padding: "10px 5px 5px 10px",
|
||||
display: "flex",
|
||||
flexGrow: 1,
|
||||
minHeight: 0, // Important for flex shrinking
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
<div style={{
|
||||
width: "30%",
|
||||
margin: "5px",
|
||||
border: "1px solid grey",
|
||||
borderRadius: "5px",
|
||||
padding: "5px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
minHeight: 0, // Important for flex shrinking
|
||||
overflow: "auto"
|
||||
}}>
|
||||
<Typography textAlign="center" variant={"h7"} style={{fontWeight: 600, width: "100%"}}>
|
||||
2. Build Parameter Configuration
|
||||
<MythicStyledTooltip title={"Edit Build Parameters"}>
|
||||
<IconButton color={"primary"} onClick={() => props.moveToStep(1)}>
|
||||
<DriveFileRenameOutlineIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</Typography>
|
||||
<ConfigurationSummary buildParameters={props.buildOptions[1].parameters} os={props.buildOptions[1].os} />
|
||||
</div>
|
||||
<div style={{
|
||||
width: "30%",
|
||||
margin: "5px",
|
||||
border: "1px solid grey",
|
||||
borderRadius: "5px",
|
||||
padding: "5px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
minHeight: 0, // Important for flex shrinking
|
||||
overflow: "auto"
|
||||
}}>
|
||||
<Typography textAlign="center" variant={"h7"} style={{fontWeight: 600, width: "100%"}}>
|
||||
3. Command Selection
|
||||
<MythicStyledTooltip title={"Edit Commands"}>
|
||||
<IconButton color={"primary"} onClick={() => props.moveToStep(2)}>
|
||||
<DriveFileRenameOutlineIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</Typography>
|
||||
{props.buildOptions[2]?.map(c => (
|
||||
<div key={c} style={{display: "flex", alignItems: "center", justifyContent: "space-between"}}>
|
||||
{c}
|
||||
<IconButton variant="contained" onClick={(e) => e.stopPropagation()}
|
||||
href={"/docs/agents/" + props.buildOptions[1].payload_type + "/commands/" + c}
|
||||
style={{marginLeft: "10px", float: "right"}} target="_blank">
|
||||
<MenuBookIcon/>
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
|
||||
</div>
|
||||
<div style={{
|
||||
width: "40%",
|
||||
margin: "5px",
|
||||
border: "1px solid grey",
|
||||
borderRadius: "5px",
|
||||
padding: "5px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
minHeight: 0, // Important for flex shrinking
|
||||
overflow: "auto"
|
||||
}}>
|
||||
<Typography textAlign="center" variant={"h7"} style={{fontWeight: 600, width: "100%"}}>
|
||||
4. C2 Configuration
|
||||
<MythicStyledTooltip title={"Edit C2 Parameters"}>
|
||||
<IconButton color={"primary"} onClick={() => props.moveToStep(3)}>
|
||||
<DriveFileRenameOutlineIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</Typography>
|
||||
{props.buildOptions[3]?.c2?.map( (c, index) => (
|
||||
<ConfigurationSummary key={c.name + index} buildParameters={c.c2profileparameters}
|
||||
os={props.buildOptions[1].os} c2_name={c.name} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation buttons - always at bottom */}
|
||||
<div style={{flexShrink: 0}}>
|
||||
<CreatePayloadNavigationButtons
|
||||
first={props.first}
|
||||
last={props.last}
|
||||
canceled={canceled}
|
||||
finished={finished}
|
||||
/>
|
||||
<br/><br/>
|
||||
</div>
|
||||
{startSubscription &&
|
||||
<PayloadSubscriptionNotification me={props.me} subscriptionID={subscriptionID} fromNow={fromNow}/>}
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
/*
|
||||
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%"}}>
|
||||
<Typography variant="h3" align="left" id="selectc2profiles" component="div"
|
||||
style={{"marginLeft": "10px"}}>
|
||||
Payload Review
|
||||
@@ -202,5 +378,4 @@ export function Step5Build(props){
|
||||
<PayloadSubscriptionNotification me={props.me} subscriptionID={subscriptionID} fromNow={fromNow}/>}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
*/
|
||||
@@ -3,7 +3,7 @@ import Stepper from '@mui/material/Stepper';
|
||||
import Step from '@mui/material/Step';
|
||||
import StepLabel from '@mui/material/StepLabel';
|
||||
import {Step1SelectOS} from './Step1SelectOS';
|
||||
import {Step2SelectPayloadType} from './Step2SelectPayloadType';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import {Step3SelectPayload} from './Step3SelectPayload';
|
||||
import {Step5Build} from './Step5Build';
|
||||
import { snackActions } from '../../utilities/Snackbar';
|
||||
@@ -20,25 +20,36 @@ export function CreatePayloadWrapper(props){
|
||||
const getStepContent = (step) => {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return <Step1SelectOS buildOptions={payload} prevData={payload[0]} finished={handleStepData} canceled={cancelStep} first={true} last={false}/>;
|
||||
return <Step1SelectOS buildOptions={payload} prevData={payload[0]} setAllData={setAllData} finished={handleStepData} canceled={cancelStep} first={true} last={false}/>;
|
||||
case 1:
|
||||
return <Step2SelectPayloadType buildOptions={payload[0]} prevData={payload[1]} finished={handleStepData} canceled={cancelStep} first={false} last={false}/>;
|
||||
return <Step1SelectOS buildOptions={payload} prevData={payload[1]} finished={handleStepData} canceled={cancelStep} first={false} last={false}/>;
|
||||
case 2:
|
||||
return <Step3SelectPayload buildOptions={payload[1]} prevData={payload[2]} finished={handleStepData} canceled={cancelStep} first={false} last={false} />;
|
||||
case 3:
|
||||
return <Step5Build buildOptions={payload} canceled={cancelStep} first={false} last={true} startOver={startOver}/>;
|
||||
return <Step5Build me={me} buildOptions={payload} moveToStep={moveToStep} canceled={cancelStep} first={false} last={true} startOver={startOver}/>;
|
||||
default:
|
||||
return 'Unknown step';
|
||||
}
|
||||
}
|
||||
const handleStepData = (stepData) => {
|
||||
setPayload({...payload, [activeStep]: stepData});
|
||||
const setAllData = (stepData) => {
|
||||
setPayload(stepData);
|
||||
handleNext();
|
||||
}
|
||||
}
|
||||
const handleStepData = (stepData, clearNextPreviousData) => {
|
||||
let newPayload = {...payload, [activeStep]: stepData};
|
||||
if(clearNextPreviousData){
|
||||
newPayload[activeStep+1] = undefined;
|
||||
}
|
||||
setPayload(newPayload);
|
||||
//console.log(activeStep, newPayload);
|
||||
handleNext();
|
||||
}
|
||||
const cancelStep = () => {
|
||||
handleBack();
|
||||
}
|
||||
|
||||
const moveToStep = (step) => {
|
||||
setActiveStep(step);
|
||||
}
|
||||
const steps = getSteps();
|
||||
|
||||
const handleNext = () => {
|
||||
@@ -59,32 +70,37 @@ export function CreatePayloadWrapper(props){
|
||||
|
||||
return (
|
||||
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%"}}>
|
||||
<Stepper activeStep={activeStep} alternativeLabel style={{marginTop: "10px"}}>
|
||||
{steps.map((label, index) => (
|
||||
<Step key={label} sx={{
|
||||
'& .MuiStepLabel-root .Mui-completed': {
|
||||
color: 'success.main', // circle color (COMPLETED)
|
||||
},
|
||||
'& .MuiStepLabel-label.Mui-completed.MuiStepLabel-alternativeLabel':
|
||||
{
|
||||
color: 'grey.500', // Just text label (COMPLETED)
|
||||
<div style={{display: "flex", flexDirection: "row", width: "100%", alignItems: "center"}}>
|
||||
<Typography variant="h5" style={{marginLeft: "10px", width: "25%"}}>
|
||||
Payload Creation
|
||||
</Typography>
|
||||
<Stepper activeStep={activeStep} alternativeLabel style={{marginTop: "10px", width: "100%"}}>
|
||||
{steps.map((label, index) => (
|
||||
<Step key={label} sx={{
|
||||
'& .MuiStepLabel-root .Mui-completed': {
|
||||
color: 'success.main', // circle color (COMPLETED)
|
||||
},
|
||||
'& .MuiStepLabel-root .Mui-active': {
|
||||
color: 'info.main', // circle color (ACTIVE)
|
||||
},
|
||||
'& .MuiStepLabel-label.Mui-active.MuiStepLabel-alternativeLabel':
|
||||
{
|
||||
fontWeight: "bold", // Just text label (ACTIVE)
|
||||
color: ''
|
||||
'& .MuiStepLabel-label.Mui-completed.MuiStepLabel-alternativeLabel':
|
||||
{
|
||||
color: 'grey.500', // Just text label (COMPLETED)
|
||||
},
|
||||
'& .MuiStepLabel-root .Mui-active': {
|
||||
color: 'info.main', // circle color (ACTIVE)
|
||||
},
|
||||
'& .MuiStepLabel-root .Mui-active .MuiStepIcon-text': {
|
||||
fill: 'black', // circle's number (ACTIVE)
|
||||
},
|
||||
}}>
|
||||
<StepLabel>{label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
'& .MuiStepLabel-label.Mui-active.MuiStepLabel-alternativeLabel':
|
||||
{
|
||||
fontWeight: "bold", // Just text label (ACTIVE)
|
||||
color: ''
|
||||
},
|
||||
'& .MuiStepLabel-root .Mui-active .MuiStepIcon-text': {
|
||||
fill: 'black', // circle's number (ACTIVE)
|
||||
},
|
||||
}}>
|
||||
<StepLabel>{label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
</div>
|
||||
<div style={{display: "flex", flexDirection: "column", flexGrow: 1, overflowY: 'auto'}}>
|
||||
{getStepContent(activeStep)}
|
||||
</div>
|
||||
|
||||
@@ -13,10 +13,12 @@ export function CreatePayloadNavigationButtons(props){
|
||||
const me = useReactiveVar(meState);
|
||||
const disabledButtons = (me?.user?.current_operation_id || 0) > 0 ? false : true;
|
||||
return (
|
||||
<div >
|
||||
<div style={{marginLeft: "20px"}}>
|
||||
<Button
|
||||
disabled={props.first}
|
||||
color="secondary"
|
||||
color="info"
|
||||
variant={"outlined"}
|
||||
style={{marginRight: "10px"}}
|
||||
onClick={props.canceled}
|
||||
>
|
||||
Back
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import React from 'react';
|
||||
import {useQuery, gql} from '@apollo/client';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Select from '@mui/material/Select';
|
||||
import { CreatePayloadNavigationButtons} from './CreatePayloadNavigationButtons';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import {snackActions} from '../../utilities/Snackbar';
|
||||
import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery";
|
||||
import {getDefaultChoices, getDefaultValueForType, getSavedToType} from "../CreatePayload/Step2SelectPayloadType";
|
||||
import { Backdrop, CircularProgress } from '@mui/material';
|
||||
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
|
||||
import {
|
||||
ConfigureBuildParameters,
|
||||
GetPayloads,
|
||||
StartFromExistingPayloadOrStartFresh
|
||||
} from "../CreatePayload/Step1SelectOS";
|
||||
|
||||
|
||||
const GET_Payload_Types = gql`
|
||||
@@ -13,81 +21,320 @@ query getPayloadTypesQuery {
|
||||
payloadtype(where: {deleted: {_eq: false}, wrapper: {_eq: true}}) {
|
||||
id
|
||||
supported_os
|
||||
name
|
||||
note
|
||||
semver
|
||||
}
|
||||
}
|
||||
`;
|
||||
const GetPayloadBuildQuery = gql`
|
||||
query getPayloadTypesBuildParametersQuery($payload_id: Int!) {
|
||||
payload_by_pk(id: $payload_id) {
|
||||
buildparameterinstances {
|
||||
build_parameter_id
|
||||
value
|
||||
}
|
||||
payloadcommands {
|
||||
command {
|
||||
cmd
|
||||
}
|
||||
}
|
||||
payloadtype {
|
||||
name
|
||||
id
|
||||
file_extension
|
||||
agent_type
|
||||
note
|
||||
supports_dynamic_loading
|
||||
supports_multiple_c2_in_build
|
||||
supports_multiple_c2_instances_in_build
|
||||
c2_parameter_deviations
|
||||
semver
|
||||
buildparameters(where: {deleted: {_eq: false} }, order_by: {description: asc}) {
|
||||
default_value
|
||||
description
|
||||
format_string
|
||||
id
|
||||
name
|
||||
parameter_type
|
||||
randomize
|
||||
required
|
||||
verifier_regex
|
||||
choices
|
||||
group_name
|
||||
supported_os
|
||||
hide_conditions
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function Step1SelectOS(props){
|
||||
const [os, setOS] = React.useState('');
|
||||
const [openBackdrop, setOpenBackdrop] = React.useState(true);
|
||||
const [payloadtypeData, setPayloadtypeData] = React.useState({});
|
||||
const [payloadtypesPerOS, setPayloadtypesPerOS] = React.useState({});
|
||||
const [osOptions, setOSOptions] = React.useState([]);
|
||||
const { loading } = useQuery(GET_Payload_Types, {fetchPolicy: "network-only",
|
||||
onCompleted: (data) => {
|
||||
const optionsReduced= data.payloadtype.reduce((currentOptions, payloadtype) => {
|
||||
const adds = payloadtype.supported_os.reduce( (prev, os) => {
|
||||
if(!currentOptions.includes(os)){
|
||||
return [...prev, os];
|
||||
}
|
||||
return prev;
|
||||
}, []);
|
||||
return [...currentOptions, ...adds];
|
||||
}, []);
|
||||
const sortedOptions = optionsReduced.sort();
|
||||
if(props.prevData !== undefined){
|
||||
setOS(props.prevData);
|
||||
}
|
||||
else if(os === "" && sortedOptions.length > 0){
|
||||
setOS(sortedOptions[0]);
|
||||
} else if(sortedOptions.length === 0 ){
|
||||
snackActions.warning("No Wrappers exist within Mythic. Try importing one first via the mythic-cli binary")
|
||||
|
||||
}
|
||||
setOSOptions(sortedOptions);
|
||||
},
|
||||
onError: (data) => {
|
||||
console.error(data);
|
||||
snackActions.error(data.message)
|
||||
}
|
||||
const [selectedPayloadType, setSelectedPayloadType] = React.useState('');
|
||||
const [payloadOptions, setPayloadOptions] = React.useState([]);
|
||||
const payloadConfigRef = React.useRef({});
|
||||
const getPayloadTypeAndPreviousData = useMythicLazyQuery(GET_Payload_Types, {fetchPolicy: "network-only",
|
||||
});
|
||||
const getPayloads = useMythicLazyQuery(GetPayloads, { fetchPolicy: "network-only",
|
||||
})
|
||||
const getPayloadConfig = useMythicLazyQuery(GetPayloadBuildQuery, { fetchPolicy: "network-only",
|
||||
})
|
||||
React.useEffect( () => {
|
||||
getPayloadTypeAndPreviousData({})
|
||||
.then(({data}) => {
|
||||
let payloadTypeOS = {};
|
||||
const payloadTypeData = data.payloadtype.reduce( (prev, cur) => {
|
||||
return {...prev, [cur.name]: {...cur}};
|
||||
}, {});
|
||||
setPayloadtypeData(payloadTypeData);
|
||||
const optionsReduced= data.payloadtype.reduce((currentOptions, payloadtype) => {
|
||||
const adds = payloadtype.supported_os.reduce( (prev, os) => {
|
||||
if(payloadTypeOS[os] === undefined){
|
||||
payloadTypeOS[os] = [payloadtype.name];
|
||||
} else {
|
||||
payloadTypeOS[os].push(payloadtype.name);
|
||||
payloadTypeOS[os].sort();
|
||||
}
|
||||
if(!currentOptions.includes(os)){
|
||||
return [...prev, os];
|
||||
}
|
||||
return prev;
|
||||
}, []);
|
||||
return [...currentOptions, ...adds];
|
||||
}, []);
|
||||
const sortedOptions = optionsReduced.sort();
|
||||
if(props.prevData !== undefined){
|
||||
setOS(props.prevData.os);
|
||||
setSelectedPayloadType(props.prevData.payload_type);
|
||||
}
|
||||
else if(os === "" && sortedOptions.length > 0){
|
||||
setOS(sortedOptions[0]);
|
||||
setSelectedPayloadType(payloadTypeOS[sortedOptions[0]][0]);
|
||||
} else if(sortedOptions.length === 0){
|
||||
snackActions.error("No Payload Types installed");
|
||||
}
|
||||
setPayloadtypesPerOS(payloadTypeOS);
|
||||
setOSOptions(sortedOptions);
|
||||
})
|
||||
.catch((data) => {console.log(data)})
|
||||
}, [props.prevData, props.first])
|
||||
React.useEffect( () => {
|
||||
setOpenBackdrop(true);
|
||||
getPayloads({variables: {payloadType: selectedPayloadType, os}})
|
||||
.then(({data}) => {
|
||||
setPayloadOptions(data.payload);
|
||||
setOpenBackdrop(false);
|
||||
})
|
||||
.catch(({data}) => {
|
||||
console.log(data);
|
||||
setOpenBackdrop(false);
|
||||
});
|
||||
}, [selectedPayloadType, os]);
|
||||
|
||||
if (loading) {
|
||||
return <div><CircularProgress /></div>;
|
||||
}
|
||||
const finished = () => {
|
||||
if(osOptions.length === 0){
|
||||
snackActions.warning("No Wrappers exist within Mythic. Try importing one first via the mythic-cli binary");
|
||||
return;
|
||||
} else if (os === ""){
|
||||
snackActions.error("Must select an operating system first");
|
||||
const finished = (clearNextPrevious) => {
|
||||
if(props.first){
|
||||
props.finished({
|
||||
"os": os,
|
||||
"payload_type": selectedPayloadType
|
||||
}, clearNextPrevious);
|
||||
} else {
|
||||
props.finished(os);
|
||||
props.finished({...payloadConfigRef.current});
|
||||
}
|
||||
|
||||
}
|
||||
const canceled = () => {
|
||||
props.canceled();
|
||||
}
|
||||
return (
|
||||
<div style={{height: "100%", display: "flex", flexDirection: "column"}}>
|
||||
<Typography variant="h3" align="left" id="selectospage" component="div"
|
||||
style={{"marginLeft": "10px"}}>
|
||||
Select Target Operating System
|
||||
</Typography> <br/>
|
||||
<div style={{flexGrow: 1, width: "100%", display: "flex", flexDirection: "column"}}>
|
||||
<Select
|
||||
value={os}
|
||||
onChange={evt => setOS(evt.target.value)}
|
||||
>
|
||||
{
|
||||
osOptions.map((opt) => (
|
||||
<MenuItem key={"step1" + opt} value={opt}>{opt}</MenuItem>
|
||||
))
|
||||
const onChangeOS = (e) => {
|
||||
setOS(e.target.value);
|
||||
setSelectedPayloadType(payloadtypesPerOS[e.target.value][0]);
|
||||
}
|
||||
const onSelectedPayload = (payload) => {
|
||||
let newConfig = {
|
||||
0: {
|
||||
"os": os,
|
||||
"payload_type": selectedPayloadType,
|
||||
"payload": payload.id,
|
||||
},
|
||||
};
|
||||
getPayloadConfig({variables: {payload_id: payload.id}})
|
||||
.then(({data}) => {
|
||||
newConfig[1] = {
|
||||
os: os,
|
||||
payload: payload.id,
|
||||
payload_type: selectedPayloadType,
|
||||
agent_type: data.payload_by_pk.payloadtype.agent_type,
|
||||
description: data.payload_by_pk.payloadtype.note,
|
||||
file_extension: data.payload_by_pk.payloadtype.file_extension,
|
||||
supports_dynamic_loading: data.payload_by_pk.payloadtype.supports_dynamic_loading,
|
||||
supports_multiple_c2_in_build: data.payload_by_pk.payloadtype.supports_multiple_c2_in_build,
|
||||
supports_multiple_c2_instances_in_build: data.payload_by_pk.payloadtype.supports_multiple_c2_instances_in_build,
|
||||
c2_parameter_deviations: data.payload_by_pk.payloadtype.c2_parameter_deviations,
|
||||
};
|
||||
const params = data.payload_by_pk.payloadtype.buildparameters.map((param) => {
|
||||
for (let p = 0; p < data.payload_by_pk.buildparameterinstances.length; p++) {
|
||||
if (data.payload_by_pk.buildparameterinstances[p]["build_parameter_id"] === param.id) {
|
||||
let value = {
|
||||
...param, error: false,
|
||||
value: data.payload_by_pk.buildparameterinstances[p]["value"],
|
||||
trackedValue: data.payload_by_pk.buildparameterinstances[p]["value"],
|
||||
initialValue: data.payload_by_pk.buildparameterinstances[p]["value"],
|
||||
choices: getDefaultChoices(param)
|
||||
};
|
||||
let newValue = getSavedToType(value);
|
||||
return {
|
||||
...param, error: false,
|
||||
value: newValue,
|
||||
trackedValue: newValue,
|
||||
initialValue: newValue,
|
||||
choices: getDefaultChoices(param)
|
||||
}
|
||||
}
|
||||
}
|
||||
</Select>
|
||||
const initialValue = getDefaultValueForType(param);
|
||||
return {
|
||||
...param, error: false,
|
||||
value: initialValue,
|
||||
trackedValue: initialValue,
|
||||
initialValue: initialValue,
|
||||
choices: getDefaultChoices(param)
|
||||
}
|
||||
});
|
||||
params.sort((a, b) => -b.description.localeCompare(a.description));
|
||||
newConfig[1].parameters = params;
|
||||
newConfig[2] = data.payload_by_pk.payloadcommands.map( c => c.command.cmd);
|
||||
props.setAllData(newConfig);
|
||||
})
|
||||
.catch((data) => console.log(data));
|
||||
|
||||
}
|
||||
const onStartFresh = () => {
|
||||
finished(true)
|
||||
}
|
||||
const onUpdatePayloadConfig = (payload) => {
|
||||
payloadConfigRef.current = payload
|
||||
}
|
||||
return (
|
||||
<div style={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column"
|
||||
}}>
|
||||
{/* Content area that can grow */}
|
||||
<div style={{
|
||||
flexGrow: 1,
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: 0 // Important for flex shrinking
|
||||
}}>
|
||||
{/* Top section - fixed height */}
|
||||
<div style={{
|
||||
display: "flex",
|
||||
flexShrink: 0 // Don't shrink this section
|
||||
}}>
|
||||
<div style={{width: "100%", margin: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px"}}>
|
||||
<Typography variant={"p"} style={{fontWeight: 600}}>
|
||||
1. Select Operating System
|
||||
</Typography>
|
||||
<Select
|
||||
value={os}
|
||||
style={{width: "100%"}}
|
||||
disabled={!props.first}
|
||||
onChange={onChangeOS}
|
||||
>
|
||||
{
|
||||
osOptions.map((opt) => (
|
||||
<MenuItem key={"step1" + opt} value={opt}>{opt}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
<Typography style={{fontWeight: 600}}>
|
||||
Compatible Payload Types
|
||||
</Typography>
|
||||
{payloadtypesPerOS[os]?.join(", ")}
|
||||
</div>
|
||||
<div style={{width: "100%", margin: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px"}}>
|
||||
<div style={{width: "100%", display: "flex", alignItems: "flex-start", marginBottom: "10px", flexDirection: "column"}}>
|
||||
<Typography style={{fontWeight: 600}} variant={"p"}>
|
||||
2. Select Payload Type
|
||||
</Typography>
|
||||
<Select
|
||||
style={{width: "100%"}}
|
||||
disabled={!props.first}
|
||||
value={selectedPayloadType}
|
||||
onChange={evt => setSelectedPayloadType(evt.target.value)}
|
||||
>
|
||||
{
|
||||
payloadtypesPerOS[os]?.map((opt) => (
|
||||
<MenuItem key={"step1" + opt} value={opt}>{opt}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
|
||||
</div>
|
||||
<div style={{display: "flex"}}>
|
||||
<MythicAgentSVGIcon payload_type={selectedPayloadType} style={{width: "80px", padding: "5px", objectFit: "unset"}} />
|
||||
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
|
||||
<b>Version: </b>{payloadtypeData[selectedPayloadType]?.semver}<br/>
|
||||
<b>Description: </b>{payloadtypeData[selectedPayloadType]?.note}<br/>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom section - scrollable table area */}
|
||||
<div style={{
|
||||
margin: "5px",
|
||||
border: props.first ? "1px solid grey" : '',
|
||||
borderRadius: "5px",
|
||||
padding: "10px 5px 5px 10px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
minHeight: 0, // Important for flex shrinking
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
{props.first &&
|
||||
<div style={{flexGrow: 1, overflowY: "auto", position: "relative"}}>
|
||||
{openBackdrop &&
|
||||
<Backdrop open={openBackdrop} onClick={()=>{setOpenBackdrop(false);}} style={{zIndex: 2000, position: "absolute"}}>
|
||||
<CircularProgress color="inherit" disableShrink />
|
||||
</Backdrop>
|
||||
}
|
||||
<StartFromExistingPayloadOrStartFresh first={props.first}
|
||||
last={props.last}
|
||||
canceled={canceled}
|
||||
onSelectedPayload={onSelectedPayload}
|
||||
payloadOptions={payloadOptions}
|
||||
onStartFresh={onStartFresh}
|
||||
/>
|
||||
</div>
|
||||
|
||||
}
|
||||
{!props.first &&
|
||||
<ConfigureBuildParameters os={os} selectedPayloadType={selectedPayloadType}
|
||||
prevData={props.prevData}
|
||||
onUpdatePayloadConfig={onUpdatePayloadConfig} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CreatePayloadNavigationButtons first={props.first} last={props.last} canceled={canceled}
|
||||
finished={finished}/>
|
||||
<br/><br/>
|
||||
{/* Navigation buttons - always at bottom */}
|
||||
<div style={{flexShrink: 0}}>
|
||||
<CreatePayloadNavigationButtons
|
||||
first={props.first}
|
||||
last={props.last}
|
||||
canceled={canceled}
|
||||
finished={finished}
|
||||
/>
|
||||
<br/><br/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
import React, { } from 'react';
|
||||
import {useQuery, gql} from '@apollo/client';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Select from '@mui/material/Select';
|
||||
import { CreatePayloadNavigationButtons} from './CreatePayloadNavigationButtons';
|
||||
import {CreatePayloadBuildParametersTable} from './CreatePayloadBuildParametersTable';
|
||||
import {snackActions} from '../../utilities/Snackbar';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import {getDefaultValueForType, getDefaultChoices} from '../CreatePayload/Step2SelectPayloadType';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
|
||||
const GET_Payload_Types = gql`
|
||||
query getPayloadTypesBuildParametersQuery($os: jsonb!) {
|
||||
payloadtype(where: {supported_os: {_contains: $os}, deleted: {_eq: false}, wrapper: {_eq: true}}, order_by: {name: asc}) {
|
||||
name
|
||||
id
|
||||
file_extension
|
||||
supports_dynamic_loading
|
||||
buildparameters(where: {deleted: {_eq: false} }, order_by: {description: asc}) {
|
||||
default_value
|
||||
description
|
||||
format_string
|
||||
id
|
||||
name
|
||||
parameter_type
|
||||
randomize
|
||||
required
|
||||
verifier_regex
|
||||
choices
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function Step2SelectPayloadType(props){
|
||||
const [selectedPayloadType, setSelectedPayloadType] = React.useState('');
|
||||
const [selectedPayloadTypeID, setSelectedPayloadTypeID] = React.useState(0);
|
||||
const [fileExtension, setFileExtension] = React.useState('');
|
||||
const [payloadTypeParameters, setSelectedPayloadTypeParameters] = React.useState([]);
|
||||
const { loading, error, data } = useQuery(GET_Payload_Types,
|
||||
{variables:{os: props.buildOptions},
|
||||
fetchPolicy: "no-cache",
|
||||
onCompleted: data => {
|
||||
if(data.payloadtype.length > 0){
|
||||
if(props.prevData !== undefined && props.prevData.os === props.buildOptions){
|
||||
|
||||
setSelectedPayloadType(props.prevData.payload_type);
|
||||
setSelectedPayloadTypeID(props.prevData.payload_type_id);
|
||||
setFileExtension(props.prevData.file_extension);
|
||||
const payloadtypedata = data.payloadtype.reduce( (prev, payload) => {
|
||||
if(payload.name === props.prevData.payload_type){
|
||||
const params = payload.buildparameters.map( (param) => {
|
||||
for(let p = 0; p < props.prevData.parameters.length; p++){
|
||||
if(props.prevData.parameters[p]["name"] === param.name){
|
||||
return {...param, error: false,
|
||||
value: props.prevData.parameters[p]["value"],
|
||||
trackedValue: props.prevData.parameters[p]["value"],
|
||||
initialValue: getDefaultValueForType(param),
|
||||
choices: getDefaultChoices(param)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return [...prev, ...params];
|
||||
}
|
||||
return [...prev];
|
||||
}, []);
|
||||
payloadtypedata.sort((a,b) => -b.description.localeCompare(a.description));
|
||||
setSelectedPayloadTypeParameters(payloadtypedata);
|
||||
if(payloadtypedata.length === 0){
|
||||
snackActions.warning("No available payload types exist for the selected OS");
|
||||
}
|
||||
}else{
|
||||
setSelectedPayloadType(data.payloadtype[0].name);
|
||||
setSelectedPayloadTypeID(data.payloadtype[0].id);
|
||||
setFileExtension(data.payloadtype[0].file_extension);
|
||||
const payloadtypedata = data.payloadtype.reduce( (prev, payload) => {
|
||||
if(payload.name === data.payloadtype[0].name){
|
||||
const params = payload.buildparameters.map( (param) => {
|
||||
const initialValue = getDefaultValueForType(param);
|
||||
return {...param, error: false, value: initialValue,
|
||||
trackedValue: initialValue,
|
||||
initialValue: initialValue,
|
||||
choices: getDefaultChoices(param)}
|
||||
});
|
||||
return [...prev, ...params];
|
||||
}
|
||||
return [...prev];
|
||||
}, []);
|
||||
payloadtypedata.sort((a,b) => -b.description.localeCompare(a.description));
|
||||
setSelectedPayloadTypeParameters(payloadtypedata);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const finished = () => {
|
||||
if(selectedPayloadType === ""){
|
||||
snackActions.warning("No payload type selected");
|
||||
return;
|
||||
}
|
||||
props.finished({"payload_type": selectedPayloadType,
|
||||
"parameters": payloadTypeParameters,
|
||||
"payload_type_id": selectedPayloadTypeID,
|
||||
"file_extension": fileExtension,
|
||||
"os": props.buildOptions});
|
||||
}
|
||||
const canceled = () => {
|
||||
props.canceled();
|
||||
}
|
||||
const changePayloadType = (evt) => {
|
||||
setSelectedPayloadType(evt.target.value);
|
||||
const payloadtypedata = data.payloadtype.reduce( (prev, payload) => {
|
||||
if(payload.name === evt.target.value){
|
||||
setFileExtension(payload.file_extension);
|
||||
setSelectedPayloadTypeID(payload.id);
|
||||
const params = payload.buildparameters.map( (param) => {
|
||||
const initialValue = getDefaultValueForType(param);
|
||||
return {...param, error: false, value: initialValue,
|
||||
trackedValue: initialValue,
|
||||
initialValue: initialValue,
|
||||
choices: getDefaultChoices(param)}
|
||||
});
|
||||
return [...prev, ...params];
|
||||
}
|
||||
return [...prev];
|
||||
}, []);
|
||||
payloadtypedata.sort((a,b) => -b.description.localeCompare(a.description));
|
||||
setSelectedPayloadTypeParameters(payloadtypedata);
|
||||
}
|
||||
const onChange = (name, value, error) => {
|
||||
const newParams = payloadTypeParameters.map( (param) => {
|
||||
if(param.name === name){
|
||||
return {...param, value, error}
|
||||
}
|
||||
return {...param};
|
||||
});
|
||||
setSelectedPayloadTypeParameters(newParams);
|
||||
}
|
||||
if (loading) {
|
||||
return <div><CircularProgress /></div>;
|
||||
}
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return <div>Error! {error.message}</div>;
|
||||
}
|
||||
return (
|
||||
<div style={{height: "100%", display: "flex", flexDirection: "column"}}>
|
||||
<Typography variant="h3" align="left" id="selectospage" component="div"
|
||||
style={{"marginLeft": "10px"}}>
|
||||
Select Target Payload Type
|
||||
</Typography>
|
||||
<Select
|
||||
value={selectedPayloadType}
|
||||
onChange={changePayloadType}
|
||||
>
|
||||
{
|
||||
data.payloadtype.map((opt) => (
|
||||
<MenuItem key={"step2" + opt.name} value={opt.name}>{opt.name}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select><br/>
|
||||
<div style={{display: "flex", flexGrow: 1, overflowY: "auto"}}>
|
||||
<CreatePayloadBuildParametersTable onChange={onChange} buildParameters={payloadTypeParameters}/>
|
||||
</div>
|
||||
<CreatePayloadNavigationButtons first={props.first} last={props.last} canceled={canceled}
|
||||
finished={finished}/>
|
||||
<br/><br/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,11 @@ import { toLocalTime } from '../../utilities/Time';
|
||||
import InfoIconOutline from '@mui/icons-material/InfoOutlined';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import {b64DecodeUnicode} from '../Callbacks/ResponseDisplay';
|
||||
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
|
||||
import { Backdrop, CircularProgress } from '@mui/material';
|
||||
import {PayloadsTableRowBuildStatus} from "../Payloads/PayloadsTableRowBuildStatus";
|
||||
import {PayloadsTableRowBuildProgress} from "../Payloads/PayloadsTableRowBuildProgress";
|
||||
|
||||
|
||||
const PREFIX = 'Step3SelectPayload';
|
||||
|
||||
@@ -60,6 +65,18 @@ query getWrappablePayloads($payloadType: Int!) {
|
||||
description
|
||||
uuid
|
||||
creation_time
|
||||
build_phase
|
||||
payload_build_steps(order_by: {step_number: asc}) {
|
||||
step_name
|
||||
step_number
|
||||
step_success
|
||||
step_skip
|
||||
start_time
|
||||
end_time
|
||||
step_stdout
|
||||
step_stderr
|
||||
id
|
||||
}
|
||||
filemetum {
|
||||
agent_file_id
|
||||
filename_text
|
||||
@@ -74,6 +91,7 @@ query getWrappablePayloads($payloadType: Int!) {
|
||||
|
||||
export function Step3SelectPayload(props){
|
||||
const [payloadOptions, setPayloadOptions] = React.useState([]);
|
||||
const [openBackdrop, setOpenBackdrop] = React.useState(true);
|
||||
useQuery(GET_Payload_Types, {fetchPolicy: "network-only", variables: {payloadType: props.buildOptions["payload_type_id"]},
|
||||
onCompleted: (data) => {
|
||||
if(data.payloadtype_by_pk.wrap_these_payload_types.length > 0){
|
||||
@@ -83,7 +101,9 @@ export function Step3SelectPayload(props){
|
||||
options.push({name: data.payloadtype_by_pk.wrap_these_payload_types[i].wrapped.name, ...data.payloadtype_by_pk.wrap_these_payload_types[i].wrapped.payloads[j] })
|
||||
}
|
||||
}
|
||||
options = options.sort((a,b) => new Date(a.creation_time) < new Date(b.creation_time) ? 1 : -1)
|
||||
setPayloadOptions(options);
|
||||
setOpenBackdrop(false);
|
||||
}else{
|
||||
snackActions.warning("No supported payload for that wrapper");
|
||||
}
|
||||
@@ -101,16 +121,71 @@ export function Step3SelectPayload(props){
|
||||
props.canceled();
|
||||
}
|
||||
return (
|
||||
<div style={{height: "100%", display: "flex", flexDirection: "column", width: '100%'}}>
|
||||
<Typography variant="h3" align="left" id="selectcommands" component="div"
|
||||
style={{"marginLeft": "10px"}}>
|
||||
Wrap Agent Into New Payload
|
||||
</Typography> <br/>
|
||||
<div style={{flexGrow: 1, overflowY: "auto"}}>
|
||||
<PayloadSelect payloadOptions={payloadOptions} first={props.first} last={props.last}
|
||||
canceled={canceled} finished={finished}/>
|
||||
<div style={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column"
|
||||
}}>
|
||||
{/* Content area that can grow */}
|
||||
<div style={{
|
||||
flexGrow: 1,
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: 0 // Important for flex shrinking
|
||||
}}>
|
||||
{/* Top section - fixed height */}
|
||||
<div style={{
|
||||
display: "flex",
|
||||
flexShrink: 0 // Don't shrink this section
|
||||
}}>
|
||||
<div style={{display: "flex", width: "100%", margin: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px"}}>
|
||||
<MythicAgentSVGIcon payload_type={props.buildOptions.payload_type} style={{width: "80px", padding: "5px", objectFit: "unset"}} />
|
||||
<div>
|
||||
<Typography variant={"p"} style={{}}>
|
||||
<b>OS: </b>{props.buildOptions.os}
|
||||
</Typography><br/>
|
||||
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
|
||||
<b>Description: </b>{props.buildOptions.description}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{width: "100%", margin: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px"}}>
|
||||
<div style={{width: "100%", display: "flex", alignItems: "flex-start", marginBottom: "10px", flexDirection: "column"}}>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom section - scrollable table area */}
|
||||
<div style={{
|
||||
margin: "5px",
|
||||
border: props.first ? "1px solid grey" : '',
|
||||
borderRadius: "5px",
|
||||
padding: "10px 5px 5px 10px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
minHeight: 0, // Important for flex shrinking
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
<Typography variant={"p"} style={{fontWeight: 600}}>
|
||||
1. Select Payload to Include
|
||||
</Typography>
|
||||
<div style={{flexGrow: 1, overflowY: "auto", position: "relative"}}>
|
||||
{openBackdrop &&
|
||||
<Backdrop open={openBackdrop} onClick={()=>{setOpenBackdrop(false);}} style={{zIndex: 2000, position: "absolute"}}>
|
||||
<CircularProgress color="inherit" disableShrink />
|
||||
</Backdrop>
|
||||
}
|
||||
<PayloadSelect payloadOptions={payloadOptions} first={props.first} last={props.last}
|
||||
canceled={canceled} finished={finished}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{paddingTop: "20px"}}>
|
||||
|
||||
{/* Navigation buttons - always at bottom */}
|
||||
<div style={{flexShrink: 0}}>
|
||||
<CreatePayloadNavigationButtons disableNext first={props.first} last={props.last}
|
||||
canceled={props.canceled} finished={finished}/>
|
||||
<br/><br/>
|
||||
@@ -119,32 +194,36 @@ export function Step3SelectPayload(props){
|
||||
);
|
||||
}
|
||||
|
||||
function PayloadSelect(props) {
|
||||
export function PayloadSelect(props) {
|
||||
const finished = (payload) => {
|
||||
props.finished(payload);
|
||||
}
|
||||
return (
|
||||
<Table stickyHeader size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
<div style={{height: "100%", overflow: "auto"}}>
|
||||
<Table stickyHeader size="small" style={{tableLayout:"fixed", maxWidth: "100%",}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{width: "4rem"}}> Select</TableCell>
|
||||
<TableCell style={{width: "6rem"}}> Select</TableCell>
|
||||
<TableCell style={{width: "15rem"}}>Timestamp</TableCell>
|
||||
<TableCell>File</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
<TableCell style={{width: "5rem"}}>Details</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
|
||||
{props.payloadOptions.map( (op) => (
|
||||
<PayloadsTableRow
|
||||
onSelected={finished}
|
||||
key={"payload" + op.id}
|
||||
payload={op}
|
||||
/>
|
||||
))}
|
||||
|
||||
{props.payloadOptions?.map( (op) => (
|
||||
<PayloadsTableRow
|
||||
onSelected={finished}
|
||||
key={"payload" + op.id}
|
||||
payload={op}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@@ -162,6 +241,10 @@ export function PayloadsTableRow(props){
|
||||
</TableCell>
|
||||
<TableCell>{toLocalTime(props.payload.creation_time, me.user.view_utc_time)}</TableCell>
|
||||
<TableCell>{b64DecodeUnicode(props.payload.filemetum.filename_text)}</TableCell>
|
||||
<TableCell>
|
||||
<PayloadsTableRowBuildStatus {...props.payload} />
|
||||
<PayloadsTableRowBuildProgress {...props.payload} />
|
||||
</TableCell>
|
||||
<TableCell>{props.payload.description}</TableCell>
|
||||
<TableCell>
|
||||
<IconButton size="small" color="info" onClick={() => setOpenDetailedView(true)}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import { gql, useMutation} from '@apollo/client';
|
||||
import { gql, useMutation, useQuery} from '@apollo/client';
|
||||
import { CreatePayloadNavigationButtons} from './CreatePayloadNavigationButtons';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import {PayloadSubscriptionNotification} from '../CreatePayload/PayloadSubscriptionNotification';
|
||||
@@ -7,6 +7,18 @@ import MythicTextField from '../../MythicComponents/MythicTextField';
|
||||
import {snackActions} from '../../utilities/Snackbar';
|
||||
import {UploadTaskFile} from "../../MythicComponents/MythicFileUpload";
|
||||
import {getSkewedNow} from "../../utilities/Time";
|
||||
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import {ConfigurationSummary, GetGroupedParameters} from "../CreatePayload/Step1SelectOS";
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import DriveFileRenameOutlineIcon from '@mui/icons-material/DriveFileRenameOutline';
|
||||
import {exportPayloadConfigQuery} from "../Payloads/PayloadsTableRow";
|
||||
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';
|
||||
|
||||
const create_payload = gql`
|
||||
mutation createPayloadMutation($payload: String!) {
|
||||
@@ -20,12 +32,12 @@ import {getSkewedNow} from "../../utilities/Time";
|
||||
|
||||
|
||||
export function Step5Build(props){
|
||||
const theme = useTheme();
|
||||
const [fromNow, setFromNow] = React.useState( (getSkewedNow().toISOString()));
|
||||
const [filename, setFilename] = React.useState("");
|
||||
const [description, setDescription] = React.useState("");
|
||||
const [startSubscription, setStartSubscription] = React.useState(false);
|
||||
const [subscriptionID, setSubscriptionID] = React.useState("");
|
||||
const [showExtraOptions, setShowExtraOptions] = React.useState(false);
|
||||
const [createPayloadMutation] = useMutation(create_payload, {
|
||||
update: (cache, {data}) => {
|
||||
if(data.createPayload.status === "success"){
|
||||
@@ -33,13 +45,29 @@ export function Step5Build(props){
|
||||
if(!startSubscription){
|
||||
setStartSubscription(true);
|
||||
}
|
||||
setShowExtraOptions(true);
|
||||
snackActions.info("Submitted payload to build pipeline", {autoClose: 1000});
|
||||
}else{
|
||||
snackActions.error(data.createPayload.error);
|
||||
}
|
||||
}
|
||||
});
|
||||
const [payloadConfig, setPayloadConfig] = React.useState("");
|
||||
useQuery(exportPayloadConfigQuery, {
|
||||
variables: {uuid: props.buildOptions[2]},
|
||||
fetchPolicy: "no-cache",
|
||||
onCompleted: (data) => {
|
||||
//console.log(data)
|
||||
if(data.exportPayloadConfig.status === "success"){
|
||||
setPayloadConfig(data.exportPayloadConfig.config);
|
||||
}else{
|
||||
snackActions.error("Failed to get configuration: " + data.exportPayloadConfig.error);
|
||||
}
|
||||
},
|
||||
onError: (data) => {
|
||||
console.log(data);
|
||||
snackActions.error("Failed to get configuration: " + data.message)
|
||||
}
|
||||
})
|
||||
useEffect( () => {
|
||||
if(props.buildOptions[1]["file_extension"] !== ""){
|
||||
setFilename(props.buildOptions[1]["payload_type"] + "." + props.buildOptions[1]["file_extension"]);
|
||||
@@ -56,8 +84,14 @@ export function Step5Build(props){
|
||||
}
|
||||
const finished = async () => {
|
||||
let buildParameters = [];
|
||||
for(let i = 0; i < props.buildOptions[1]["parameters"].length; i++){
|
||||
let param = props.buildOptions[1]["parameters"][i];
|
||||
let params = GetGroupedParameters({
|
||||
buildParameters: props.buildOptions[1]["parameters"],
|
||||
os: props.buildOptions[1].os,
|
||||
c2_name: undefined}).reduce( (prev, cur) => {
|
||||
return [...prev, ...cur.parameters];
|
||||
}, []);
|
||||
for(let i = 0; i < params.length; i++){
|
||||
let param = params[i];
|
||||
if (param.parameter_type === "Dictionary") {
|
||||
const newDict = param.value.reduce((prev, cur) => {
|
||||
if (cur.default_show) {
|
||||
@@ -69,6 +103,8 @@ export function Step5Build(props){
|
||||
} else if (param.parameter_type === "File") {
|
||||
if (typeof param.value === "string") {
|
||||
buildParameters.push({name: param.name, value: param.value});
|
||||
} else if(param.value.legacy){
|
||||
buildParameters.push({name: param.name, value: param.value.name});
|
||||
} else {
|
||||
const newUUID = await UploadTaskFile(param.value, "Uploaded as build parameter for " + filename);
|
||||
if (newUUID) {
|
||||
@@ -80,11 +116,13 @@ export function Step5Build(props){
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if(param.parameter_type === "FileMultiple") {
|
||||
}else if(param.parameter_type === "FileMultiple"){
|
||||
let fileMultipleValues = [];
|
||||
for(let j = 0; j < param.value.length; j++){
|
||||
if (typeof param.value[j] === "string") {
|
||||
fileMultipleValues.push(param.value[j]);
|
||||
}else if(param.value[j].legacy){
|
||||
fileMultipleValues.push(param.value[j].name);
|
||||
} else {
|
||||
const newUUID = await UploadTaskFile(param.value[j], "Uploaded as build parameter for " + filename);
|
||||
if (newUUID) {
|
||||
@@ -103,7 +141,7 @@ export function Step5Build(props){
|
||||
}
|
||||
}
|
||||
const finishedPayload = {
|
||||
"selected_os": props.buildOptions[0],
|
||||
"selected_os": props.buildOptions[1].os,
|
||||
"payload_type": props.buildOptions[1]["payload_type"],
|
||||
"filename": filename,
|
||||
"description": description,
|
||||
@@ -120,21 +158,139 @@ export function Step5Build(props){
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%"}}>
|
||||
<Typography variant="h3" align="left" id="selectc2profiles" component="div"
|
||||
style={{"marginLeft": "10px"}}>
|
||||
Payload Review
|
||||
</Typography>
|
||||
<br/>
|
||||
<div style={{display: "flex", flexDirection: "column", flexGrow: 1}}>
|
||||
<MythicTextField onEnter={finished} autoFocus={true} required={false} placeholder={"Filename"} value={filename} multiline={false} onChange={onChangeFilename} display="inline-block"/>
|
||||
<MythicTextField onEnter={finished} required={false} placeholder={"description"} value={description} multiline={false} onChange={onChangeDescription} display="inline-block"/>
|
||||
<div style={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column"
|
||||
}}>
|
||||
{/* Content area that can grow */}
|
||||
<div style={{
|
||||
flexGrow: 1,
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: 0 // Important for flex shrinking
|
||||
}}>
|
||||
{/* Top section - fixed height */}
|
||||
<div style={{
|
||||
display: "flex",
|
||||
flexShrink: 0 // Don't shrink this section
|
||||
}}>
|
||||
<div style={{width: "100%", margin: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px", display: "flex"}}>
|
||||
<MythicAgentSVGIcon payload_type={props.buildOptions[1].payload_type} style={{width: "80px", padding: "5px", objectFit: "unset"}} />
|
||||
<div>
|
||||
<Typography variant={"p"} style={{}}>
|
||||
<b>OS: </b>{props.buildOptions[1].os}
|
||||
<MythicStyledTooltip title={"Edit OS / Payload Type"}>
|
||||
<IconButton color={"primary"} onClick={() => props.moveToStep(0)}>
|
||||
<DriveFileRenameOutlineIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</Typography><br/>
|
||||
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
|
||||
<b>Description: </b>{props.buildOptions[1].description}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{width: "100%", margin: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px"}}>
|
||||
<Typography variant={"p"} style={{fontWeight: 600}}>
|
||||
1. Provide Payload Name and Description
|
||||
</Typography>
|
||||
<div style={{width: "100%", display: "flex", alignItems: "flex-start", marginBottom: "10px", flexDirection: "column"}}>
|
||||
<MythicTextField onEnter={finished} autoFocus={true} required={false} placeholder={"Filename"}
|
||||
value={filename} multiline={false} onChange={onChangeFilename} display="inline-block"/>
|
||||
<MythicTextField onEnter={finished} required={false} placeholder={"description"} value={description}
|
||||
multiline={false} onChange={onChangeDescription} display="inline-block"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom section - scrollable table area */}
|
||||
<div style={{
|
||||
margin: "5px",
|
||||
// border: "1px solid grey",
|
||||
borderRadius: "5px",
|
||||
//padding: "10px 5px 5px 10px",
|
||||
display: "flex",
|
||||
flexGrow: 1,
|
||||
minHeight: 0, // Important for flex shrinking
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
<div style={{
|
||||
width: "30%",
|
||||
margin: "5px",
|
||||
border: "1px solid grey",
|
||||
borderRadius: "5px",
|
||||
padding: "5px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
minHeight: 0, // Important for flex shrinking
|
||||
overflow: "auto"
|
||||
}}>
|
||||
<Typography textAlign="center" variant={"h7"} style={{fontWeight: 600, width: "100%"}}>
|
||||
2. Build Parameter Configuration
|
||||
<MythicStyledTooltip title={"Edit Build Parameters"}>
|
||||
<IconButton color={"primary"} onClick={() => props.moveToStep(1)}>
|
||||
<DriveFileRenameOutlineIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</Typography>
|
||||
<ConfigurationSummary buildParameters={props.buildOptions[1].parameters} os={props.buildOptions[1].os} />
|
||||
</div>
|
||||
<div style={{
|
||||
width: "70%",
|
||||
margin: "5px",
|
||||
border: "1px solid grey",
|
||||
borderRadius: "5px",
|
||||
padding: "5px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
minHeight: 0, // Important for flex shrinking
|
||||
overflow: "auto"
|
||||
}}>
|
||||
<Typography textAlign="center" variant={"h7"} style={{fontWeight: 600, width: "100%"}}>
|
||||
3. Embedded Payload Configuration
|
||||
<MythicStyledTooltip title={"Edit Selected Payload"}>
|
||||
<IconButton color={"primary"} onClick={() => props.moveToStep(2)}>
|
||||
<DriveFileRenameOutlineIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</Typography>
|
||||
<div style={{height: "100%", }}>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
|
||||
width="100%"
|
||||
height={"100%"}
|
||||
showPrintMargin={false}
|
||||
wrapEnabled={true}
|
||||
readOnly={true}
|
||||
minLines={10}
|
||||
value={payloadConfig}
|
||||
setOptions={{
|
||||
useWorker: false
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<CreatePayloadNavigationButtons first={props.first} last={props.last} canceled={canceled}
|
||||
finished={finished} startOver={props.startOver} showExtraOptions={showExtraOptions} />
|
||||
<br/><br/>
|
||||
{startSubscription && <PayloadSubscriptionNotification me={props.me} subscriptionID={subscriptionID} fromNow={fromNow}/>}
|
||||
|
||||
{/* Navigation buttons - always at bottom */}
|
||||
<div style={{flexShrink: 0}}>
|
||||
<CreatePayloadNavigationButtons
|
||||
first={props.first}
|
||||
last={props.last}
|
||||
canceled={canceled}
|
||||
finished={finished}
|
||||
/>
|
||||
<br/><br/>
|
||||
</div>
|
||||
{startSubscription &&
|
||||
<PayloadSubscriptionNotification me={props.me} subscriptionID={subscriptionID} fromNow={fromNow}/>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import {EventFeedTable} from './EventFeedTable';
|
||||
import {snackActions} from '../../utilities/Snackbar';
|
||||
import {alertCount} from "../../../cache";
|
||||
|
||||
const GET_Event_Feed = gql`
|
||||
const GET_Event_Feed_Warnings = gql`
|
||||
query GetOperationEventLogs($offset: Int!, $limit: Int!, $search: String!, $level: String!, $resolved: Boolean!) {
|
||||
operationeventlog(where: {deleted: {_eq: false}, message: {_ilike: $search}, level: {_like: $level}, resolved: {_eq: $resolved}}, order_by: {id: desc}, limit: $limit, offset: $offset) {
|
||||
operationeventlog(where: {deleted: {_eq: false}, message: {_ilike: $search}, level: {_like: $level}, resolved: {_eq: $resolved}, warning: {_eq: true}}, order_by: {id: desc}, limit: $limit, offset: $offset) {
|
||||
id
|
||||
level
|
||||
message
|
||||
@@ -14,17 +14,18 @@ query GetOperationEventLogs($offset: Int!, $limit: Int!, $search: String!, $leve
|
||||
timestamp
|
||||
count
|
||||
source
|
||||
warning
|
||||
}
|
||||
operationeventlog_aggregate(where: {deleted: {_eq: false}, message: {_ilike: $search}, level: {_like: $level}, resolved: {_eq: $resolved}}) {
|
||||
operationeventlog_aggregate(where: {deleted: {_eq: false}, message: {_ilike: $search}, level: {_like: $level}, resolved: {_eq: $resolved}, warning: {_eq: true}}) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const GET_Event_Feed_No_Resolved = gql`
|
||||
const GET_Event_Feed_No_Warnings = gql`
|
||||
query GetOperationEventLogs($offset: Int!, $limit: Int!, $search: String!, $level: String!) {
|
||||
operationeventlog(where: {deleted: {_eq: false}, message: {_ilike: $search}, level: {_like: $level}}, order_by: {id: desc}, limit: $limit, offset: $offset) {
|
||||
operationeventlog(where: {deleted: {_eq: false}, message: {_ilike: $search}, level: {_ilike: $level}}, order_by: {id: desc}, limit: $limit, offset: $offset) {
|
||||
id
|
||||
level
|
||||
message
|
||||
@@ -32,8 +33,9 @@ query GetOperationEventLogs($offset: Int!, $limit: Int!, $search: String!, $leve
|
||||
timestamp
|
||||
count
|
||||
source
|
||||
warning
|
||||
}
|
||||
operationeventlog_aggregate(where: {deleted: {_eq: false}, message: {_ilike: $search}, level: {_like: $level}}) {
|
||||
operationeventlog_aggregate(where: {deleted: {_eq: false}, message: {_ilike: $search}, level: {_ilike: $level}}) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
@@ -50,6 +52,7 @@ subscription GetOperationEventLogs($fromNow: timestamp!) {
|
||||
timestamp
|
||||
count
|
||||
source
|
||||
warning
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -63,15 +66,16 @@ mutation UpdateResolutionOperationEventLog($id: Int!, $resolved: Boolean!) {
|
||||
`;
|
||||
const Update_Level = gql`
|
||||
mutation UpdateLevelOperationEventLog($id: Int!) {
|
||||
update_operationeventlog_by_pk(pk_columns: {id: $id}, _set: {level: "warning"}) {
|
||||
update_operationeventlog_by_pk(pk_columns: {id: $id}, _set: {warning: true, resolved: false}) {
|
||||
id
|
||||
level
|
||||
warning
|
||||
resolved
|
||||
}
|
||||
}
|
||||
`;
|
||||
const Update_ResolveViewableErrors = gql`
|
||||
mutation UpdateResolveViewableErrorsOperationEventLog($ids: [Int]!) {
|
||||
update_operationeventlog(where:{id: {_in: $ids}}, _set: {resolved: true}) {
|
||||
update_operationeventlog(where:{id: {_in: $ids}, warning: {_eq: true}}, _set: {resolved: true}) {
|
||||
returning{
|
||||
id
|
||||
resolved
|
||||
@@ -81,7 +85,7 @@ mutation UpdateLevelOperationEventLog($id: Int!) {
|
||||
`;
|
||||
const Update_ResolveAllErrors = gql`
|
||||
mutation UpdateResolveAllErrorsOperationEventLog {
|
||||
update_operationeventlog(where: {level: {_eq: "warning"}, resolved: {_eq: false}}, _set: {resolved: true}) {
|
||||
update_operationeventlog(where: {resolved: {_eq: false}, warning: {_eq: true}}, _set: {resolved: true}) {
|
||||
returning{
|
||||
id
|
||||
resolved
|
||||
@@ -89,6 +93,9 @@ mutation UpdateLevelOperationEventLog($id: Int!) {
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const levelOptions = [
|
||||
"All Levels", "warning (unresolved)", "warning (resolved)", "info", "debug", "api", "auth", "agent"
|
||||
];
|
||||
export function EventFeed({}){
|
||||
const [pageData, setPageData] = React.useState({
|
||||
"totalCount": 0,
|
||||
@@ -108,18 +115,41 @@ export function EventFeed({}){
|
||||
updatingPrev[indx] = cur;
|
||||
return [...updatingPrev];
|
||||
}
|
||||
return [...prev, cur];
|
||||
// only add this if this msg fits into the right current view
|
||||
switch(level){
|
||||
case "All Levels":
|
||||
return [...prev, cur];
|
||||
case "warning (unresolved)":
|
||||
if( (cur.warning || cur.level === 'warning') && cur.resolved === false){
|
||||
return [...prev, cur]
|
||||
} else {
|
||||
return [...prev];
|
||||
}
|
||||
case "warning (resolved)":
|
||||
if( (cur.warning || cur.level === 'warning') && cur.resolved === true){
|
||||
return [...prev, cur];
|
||||
} else {
|
||||
return [...prev];
|
||||
}
|
||||
default:
|
||||
if(cur.level === level && !cur.warning){
|
||||
return [...prev, cur];
|
||||
} else {
|
||||
return [...prev];
|
||||
}
|
||||
}
|
||||
//return [...prev, cur];
|
||||
}, [...operationeventlog]);
|
||||
newEvents.sort((a,b) => (a.id > b.id) ? -1 : ((b.id > a.id) ? 1 : 0));
|
||||
setOperationEventLog(newEvents);
|
||||
}
|
||||
});
|
||||
|
||||
const [getMoreTasking] = useLazyQuery(GET_Event_Feed, {
|
||||
const [getMoreEventFeedWithWarning] = useLazyQuery(GET_Event_Feed_Warnings, {
|
||||
onError: data => {
|
||||
console.error(data)
|
||||
},
|
||||
fetchPolicy: "network-only",
|
||||
fetchPolicy: "no-cache",
|
||||
onCompleted: (data) => {
|
||||
snackActions.dismiss();
|
||||
let tempPageData = {...pageData};
|
||||
@@ -130,11 +160,11 @@ export function EventFeed({}){
|
||||
setOperationEventLog(newEventLog);
|
||||
}
|
||||
});
|
||||
const [getMoreTaskingNoResolved] = useLazyQuery(GET_Event_Feed_No_Resolved, {
|
||||
const [getMoreEventFeed] = useLazyQuery(GET_Event_Feed_No_Warnings, {
|
||||
onError: data => {
|
||||
console.error(data)
|
||||
},
|
||||
fetchPolicy: "network-only",
|
||||
fetchPolicy: "no-cache",
|
||||
onCompleted: (data) => {
|
||||
snackActions.dismiss();
|
||||
let tempPageData = {...pageData};
|
||||
@@ -162,7 +192,7 @@ export function EventFeed({}){
|
||||
const updatedMessage = data.update_operationeventlog_by_pk;
|
||||
const updatedMessages = operationeventlog.map( (log) => {
|
||||
if(log.id === updatedMessage.id){
|
||||
return {...log, level: updatedMessage.level};
|
||||
return {...log, warning: updatedMessage.warning, resolved: updatedMessage.resolved};
|
||||
}
|
||||
return log;
|
||||
});
|
||||
@@ -230,19 +260,21 @@ export function EventFeed({}){
|
||||
localLevel = "%_%";
|
||||
} else if(localLevel === "warning (unresolved)"){
|
||||
localResolved = false;
|
||||
localLevel = "warning";
|
||||
localLevel = "%_%";
|
||||
} else if(localLevel === "warning (resolved)"){
|
||||
localResolved = true;
|
||||
localLevel = "warning";
|
||||
localLevel = "%_%";
|
||||
} else {
|
||||
localLevel = "%" + localLevel + "%";
|
||||
}
|
||||
if(localResolved === undefined){
|
||||
getMoreTaskingNoResolved({variables: {offset: (value - 1) * pageData.fetchLimit,
|
||||
getMoreEventFeed({variables: {offset: (value - 1) * pageData.fetchLimit,
|
||||
limit: pageData.fetchLimit,
|
||||
search: localSearch,
|
||||
level: localLevel
|
||||
}})
|
||||
} else {
|
||||
getMoreTasking({variables: {offset: (value - 1) * pageData.fetchLimit,
|
||||
getMoreEventFeedWithWarning({variables: {offset: (value - 1) * pageData.fetchLimit,
|
||||
limit: pageData.fetchLimit,
|
||||
search: localSearch,
|
||||
level: localLevel,
|
||||
@@ -252,13 +284,9 @@ export function EventFeed({}){
|
||||
|
||||
}
|
||||
React.useEffect( () => {
|
||||
if( alertCount() > 0){
|
||||
} else {
|
||||
getMoreTaskingNoResolved({variables: {offset: 0,
|
||||
limit: pageData.fetchLimit, search: "%_%", level: "%_%"
|
||||
}});
|
||||
if( alertCount() === 0 ){
|
||||
getMoreEventFeed({variables: {offset: 0, limit: pageData.fetchLimit, search: "%_%", level: "%info%"}});
|
||||
}
|
||||
|
||||
}, [])
|
||||
const resolveViewableErrors = useCallback( () => {
|
||||
snackActions.info("Resolving Errors...");
|
||||
|
||||
@@ -14,6 +14,9 @@ import MenuItem from '@mui/material/MenuItem';
|
||||
import Select from '@mui/material/Select';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import {alertCount} from "../../../cache";
|
||||
import {levelOptions} from "./EventFeed";
|
||||
import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
|
||||
import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
|
||||
|
||||
const EventList = ({onUpdateLevel, onUpdateResolution, operationeventlog}) => {
|
||||
return (
|
||||
@@ -31,9 +34,7 @@ export function EventFeedTable(props){
|
||||
const theme = useTheme();
|
||||
const [search, setSearch] = React.useState("");
|
||||
const [level, setLevel] = React.useState("info");
|
||||
const levelOptions = [
|
||||
"All Levels", "warning (unresolved)", "warning (resolved)", "info", "debug", "api"
|
||||
];
|
||||
|
||||
|
||||
const handleSearchValueChange = (name, value, error) => {
|
||||
setSearch(value);
|
||||
@@ -52,42 +53,41 @@ export function EventFeedTable(props){
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<div style={{display: "flex", flexDirection: "column", height: "100%", maxWidth: "100%", overflowX: "hidden"}}>
|
||||
<Paper elevation={5} style={{backgroundColor: theme.body, marginBottom: "5px", marginRight: "5px"}}>
|
||||
<Grid container spacing={1} style={{ marginTop: "0px"}}>
|
||||
<Grid style={{paddingTop: 0}} size={10}>
|
||||
<MythicTextField placeholder="Search..." value={search} marginBottom={"0px"}
|
||||
onChange={handleSearchValueChange} onEnter={submitSearch} InputProps={{
|
||||
endAdornment:
|
||||
<React.Fragment>
|
||||
<Tooltip title="Search">
|
||||
<IconButton onClick={submitSearch} size="large"><SearchIcon style={{color: theme.palette.info.main}}/></IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Resolve Viewable Errors">
|
||||
<IconButton onClick={props.resolveViewableErrors} size="large"><AutoFixHighIcon style={{color: theme.palette.success.main}}/></IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Resolve All Errors">
|
||||
<IconButton onClick={props.resolveAllErrors} size="large"><HealingIcon style={{color: theme.palette.success.main}}/></IconButton>
|
||||
</Tooltip>
|
||||
</React.Fragment>,
|
||||
style: {padding: 0}
|
||||
}}/>
|
||||
</Grid>
|
||||
<Grid style={{paddingTop: 0, paddingLeft: 0}} size={2}>
|
||||
<Select
|
||||
style={{width: "100%"}}
|
||||
value={level}
|
||||
onChange={handleLevelValueChange}
|
||||
>
|
||||
{
|
||||
levelOptions.map((opt, i) => (
|
||||
<MenuItem key={"levelFilter" + opt} value={opt}>{opt}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
</Grid>
|
||||
<MythicPageBody>
|
||||
<Grid container spacing={1} style={{ marginTop: "0px"}}>
|
||||
<Grid style={{paddingTop: 0}} size={10}>
|
||||
<MythicTextField placeholder="Search..." value={search} marginBottom={"0px"} marginTop={"0px"}
|
||||
onChange={handleSearchValueChange} onEnter={submitSearch} InputProps={{
|
||||
endAdornment:
|
||||
<React.Fragment>
|
||||
<Tooltip title="Search">
|
||||
<IconButton onClick={submitSearch} size="large"><SearchIcon style={{color: theme.palette.info.main}}/></IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Resolve Viewable Errors">
|
||||
<IconButton onClick={props.resolveViewableErrors} size="large"><AutoFixHighIcon style={{color: theme.palette.success.main}}/></IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Resolve All Errors">
|
||||
<IconButton onClick={props.resolveAllErrors} size="large"><HealingIcon style={{color: theme.palette.success.main}}/></IconButton>
|
||||
</Tooltip>
|
||||
</React.Fragment>,
|
||||
style: {padding: 0}
|
||||
}}/>
|
||||
</Grid>
|
||||
</Paper>
|
||||
<Grid style={{paddingTop: 0, paddingLeft: 0}} size={2}>
|
||||
<Select
|
||||
style={{width: "100%"}}
|
||||
value={level}
|
||||
onChange={handleLevelValueChange}
|
||||
>
|
||||
{
|
||||
levelOptions.map((opt, i) => (
|
||||
<MenuItem key={"levelFilter" + opt} value={opt}>{opt}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<div style={{display: "flex", flexDirection: "column", overflowY: "auto", flexGrow: 1, overflowX: "Hidden"}}>
|
||||
<EventList
|
||||
onUpdateResolution={props.onUpdateResolution}
|
||||
@@ -99,6 +99,6 @@ export function EventFeedTable(props){
|
||||
siblingCount={1} onChange={props.onChangePage} showFirstButton={true} showLastButton={true} style={{padding: "20px"}}/>
|
||||
<Typography style={{paddingLeft: "10px"}}>Total Results: {props.pageData.totalCount}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</MythicPageBody>
|
||||
);
|
||||
}
|
||||
@@ -33,7 +33,7 @@ const StyledListItem = styled(ListItem)((
|
||||
const GetPreAdornment = ({message}) => {
|
||||
const theme = useTheme();
|
||||
const getColor = React.useCallback(() => {
|
||||
if(message.level === "warning"){
|
||||
if(message.warning){
|
||||
if(message.resolved){
|
||||
return theme.palette.success.main;
|
||||
} else {
|
||||
@@ -41,9 +41,9 @@ const GetPreAdornment = ({message}) => {
|
||||
}
|
||||
}
|
||||
return theme.palette.info.main;
|
||||
}, [theme, message.level, message.resolved]);
|
||||
}, [theme, message.level, message.resolved, message.warning]);
|
||||
const getSymbol = React.useCallback(() => {
|
||||
if(message.level === "warning"){
|
||||
if(message.warning){
|
||||
if(message.resolved){
|
||||
return "+";
|
||||
} else {
|
||||
@@ -51,7 +51,7 @@ const GetPreAdornment = ({message}) => {
|
||||
}
|
||||
}
|
||||
return "*";
|
||||
}, [theme, message.level, message.resolved]);
|
||||
}, [theme, message.level, message.resolved, message.warning]);
|
||||
return (
|
||||
<>
|
||||
<p style={{display: "inline-block", fontWeight: "bold", color: getColor(), margin: "0 0 0 0"}}>[</p>
|
||||
@@ -97,7 +97,7 @@ export function EventFeedTableEvents(props){
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<EventFeedTableEventsActions id={props.id} level={props.level}
|
||||
<EventFeedTableEventsActions id={props.id} level={props.level} warning={props.warning}
|
||||
onUpdateResolution={props.onUpdateResolution}
|
||||
onUpdateLevel={props.onUpdateLevel}
|
||||
resolved={props.resolved}/>
|
||||
|
||||
@@ -32,9 +32,9 @@ export function EventFeedTableEventsActions(props){
|
||||
</IconButton>
|
||||
<Menu elevation={5} id={"eventmenu" + props.id} anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleClose} >
|
||||
{
|
||||
props.level === "warning" && props.resolved ? (
|
||||
props.warning && props.resolved ? (
|
||||
<MenuItem onClick={onUpdateResolution}>Unresolve</MenuItem>
|
||||
) : ( props.level === "warning" && !props.resolved ? (
|
||||
) : ( props.warning && !props.resolved ? (
|
||||
<MenuItem onClick={onUpdateResolution}>Resolve</MenuItem>
|
||||
) : (
|
||||
<MenuItem onClick={onUpdateLevel}>Make Warning</MenuItem>
|
||||
|
||||
@@ -149,6 +149,16 @@ const triggerOptionsData = {
|
||||
trackedValue: [],
|
||||
error: false,
|
||||
description: "a list of all the payload types where you want this to trigger. If you don't specify any, then it will trigger for all payload types."
|
||||
},
|
||||
{
|
||||
name: "selected_os",
|
||||
parameter_type: "Array",
|
||||
default_value: "[]",
|
||||
value: [],
|
||||
initialValue: [],
|
||||
trackedValue: [],
|
||||
error: false,
|
||||
description: "a list of all the selected os values you want this to trigger on. If you don't specify any, then it will trigger for all OS types."
|
||||
}
|
||||
],
|
||||
env: payloadFields,
|
||||
@@ -165,23 +175,66 @@ const triggerOptionsData = {
|
||||
trackedValue: [],
|
||||
error: false,
|
||||
description: "a list of all the payload types where you want this to trigger. If you don't specify any, then it will trigger for all payload types."
|
||||
},
|
||||
{
|
||||
name: "selected_os",
|
||||
parameter_type: "Array",
|
||||
default_value: "[]",
|
||||
value: [],
|
||||
initialValue: [],
|
||||
trackedValue: [],
|
||||
error: false,
|
||||
description: "a list of all the selected os values you want this to trigger on. If you don't specify any, then it will trigger for all OS types."
|
||||
}
|
||||
],
|
||||
env: payloadFields,
|
||||
},
|
||||
task_create: {
|
||||
description: "This workflow is triggered when a Task is first created and sent for preprocessing at the payloadtype container",
|
||||
trigger_data: [],
|
||||
trigger_data: [
|
||||
{
|
||||
name: "payload_types_commands",
|
||||
parameter_type: "MapArray",
|
||||
default_value: "{}",
|
||||
value: {},
|
||||
initialValue: {},
|
||||
trackedValue: {},
|
||||
error: false,
|
||||
description: "a dictionary of payload type names to a list of all the command names where you want this workflow to trigger. If you specify a payload type name and no commands, then it will trigger for all commands for that payload type. Not specifying any payload types will trigger for all commands for all payload types."
|
||||
}
|
||||
],
|
||||
env: taskFields,
|
||||
},
|
||||
task_start: {
|
||||
description: "This workflow is triggered when a Task is picked up by the agent to start executing",
|
||||
trigger_data: [],
|
||||
trigger_data: [
|
||||
{
|
||||
name: "payload_types_commands",
|
||||
parameter_type: "MapArray",
|
||||
default_value: "{}",
|
||||
value: {},
|
||||
initialValue: {},
|
||||
trackedValue: {},
|
||||
error: false,
|
||||
description: "a dictionary of payload type names to a list of all the command names where you want this workflow to trigger. If you specify a payload type name and no commands, then it will trigger for all commands for that payload type. Not specifying any payload types will trigger for all commands for all payload types."
|
||||
}
|
||||
],
|
||||
env: taskFields,
|
||||
},
|
||||
task_finish: {
|
||||
description: "This workflow is triggered when a Task finishes either successfully or via error",
|
||||
trigger_data: [],
|
||||
trigger_data: [
|
||||
{
|
||||
name: "payload_types_commands",
|
||||
parameter_type: "MapArray",
|
||||
default_value: "{}",
|
||||
value: {},
|
||||
initialValue: {},
|
||||
trackedValue: {},
|
||||
error: false,
|
||||
description: "a dictionary of payload type names to a list of all the command names where you want this workflow to trigger. If you specify a payload type name and no commands, then it will trigger for all commands for that payload type. Not specifying any payload types will trigger for all commands for all payload types."
|
||||
}
|
||||
],
|
||||
env: taskFields,
|
||||
},
|
||||
user_output: {
|
||||
@@ -221,6 +274,16 @@ const triggerOptionsData = {
|
||||
trackedValue: [],
|
||||
error: false,
|
||||
description: "a list of all the payload types where you want this to trigger. If you don't specify any, then it will trigger for all payload types."
|
||||
},
|
||||
{
|
||||
name: "selected_os",
|
||||
parameter_type: "Array",
|
||||
default_value: "[]",
|
||||
value: [],
|
||||
initialValue: [],
|
||||
trackedValue: [],
|
||||
error: false,
|
||||
description: "a list of all the selected os values you want this to trigger on. If you don't specify any, then it will trigger for all OS types."
|
||||
}
|
||||
],
|
||||
env: callbackFields,
|
||||
@@ -237,18 +300,40 @@ const triggerOptionsData = {
|
||||
trackedValue: [],
|
||||
error: false,
|
||||
description: "a list of all the payload types where you want this to trigger. If you don't specify any, then it will trigger for all payload types."
|
||||
},
|
||||
{
|
||||
name: "selected_os",
|
||||
parameter_type: "Array",
|
||||
default_value: "[]",
|
||||
value: [],
|
||||
initialValue: [],
|
||||
trackedValue: [],
|
||||
error: false,
|
||||
description: "a list of all the selected os values you want this to trigger on. If you don't specify any, then it will trigger for all OS types."
|
||||
}
|
||||
],
|
||||
env: [...callbackFields, "previous_checkin", "checkin_difference"],
|
||||
},
|
||||
task_intercept: {
|
||||
description: "This workflow is triggered after a Task finishes its opsec_post check to allow one more chance for a task to be blocked.",
|
||||
trigger_data: [],
|
||||
trigger_data: [
|
||||
{
|
||||
name: "payload_types_commands",
|
||||
parameter_type: "MapArray",
|
||||
default_value: "{}",
|
||||
value: {},
|
||||
initialValue: {},
|
||||
trackedValue: {},
|
||||
error: false,
|
||||
description: "a dictionary of payload type names to a list of all the command names where you want this workflow to trigger. If you specify a payload type name and no commands, then it will trigger for all commands for that payload type. Not specifying any payload types will trigger for all commands for all payload types."
|
||||
}
|
||||
],
|
||||
env: taskFields,
|
||||
},
|
||||
response_intercept: {
|
||||
description: "This workflow is triggered when a Task returns new output in the user_output field for the user to see in the UI, but first passes that output to this workflow for modification before saving it in the database.",
|
||||
trigger_data: [],
|
||||
trigger_data: [
|
||||
],
|
||||
env: []
|
||||
},
|
||||
tag_create: {
|
||||
@@ -264,7 +349,8 @@ const triggerOptionsData = {
|
||||
error: false,
|
||||
description: "a list of all the tag type names where you want this to trigger. If you don't specify any, then it will trigger for all tag types."
|
||||
}
|
||||
]
|
||||
],
|
||||
env: tagFields,
|
||||
}
|
||||
}
|
||||
const actionOptions = ["payload_create", "callback_create", "task_create", "custom_function", "conditional_check", "task_intercept", "response_intercept", "alert_create", "webhook_send"].sort();
|
||||
@@ -548,10 +634,10 @@ const CreateEventingStep1 = ({finished, back, first, last, cancel, prevData}) =>
|
||||
<>
|
||||
<div style={{flexGrow: 1, width: "100%", display: "flex", flexDirection: "column", overflowY: "auto"}}>
|
||||
<Table style={{tableLayout: "fixed", width: "100%",}}>
|
||||
<TableHead>
|
||||
<TableHead >
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "10rem"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "10rem"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -600,8 +686,8 @@ const CreateEventingStep1 = ({finished, back, first, last, cancel, prevData}) =>
|
||||
(<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -987,8 +1073,8 @@ const EventingStepActionDataTaskCreate = ({allSteps, updateStep, index, prevData
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -1178,8 +1264,8 @@ const EventingStepActionDataCustomFunction = ({allSteps, updateStep, index, prev
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -1302,8 +1388,8 @@ const EventingStepActionDataConditionalCheck = ({allSteps, updateStep, index, pr
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -1446,8 +1532,8 @@ const EventingStepActionDataCreatePayload = ({allSteps, updateStep, index, prevD
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -1529,8 +1615,8 @@ const EventingStepActionDataCreateCallback = ({allSteps, updateStep, index, prev
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -1590,8 +1676,8 @@ const EventingStepActionDataTaskIntercept = ({allSteps, updateStep, index, prevD
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -1652,8 +1738,8 @@ const EventingStepActionDataResponseIntercept = ({allSteps, updateStep, index, p
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -1743,8 +1829,8 @@ const EventingStepActionDataCreateAlert = ({allSteps, updateStep, index, prevDat
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -1889,8 +1975,8 @@ const EventingStepActionDataSendWebhook = ({allSteps, updateStep, index, prevDat
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "30%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -2051,8 +2137,8 @@ const EventingStep = ({step, allSteps, updateStep, index, step1Data}) => {
|
||||
<Table >
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "15%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "15%"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -2162,7 +2248,7 @@ const EventingStep = ({step, allSteps, updateStep, index, step1Data}) => {
|
||||
<MythicStyledTableCell>Continue on Error</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<Switch
|
||||
checked={step.continue_on_error}
|
||||
checked={continueOnError}
|
||||
onChange={onChangeContinueOnError}
|
||||
color={"info"}
|
||||
inputProps={{ 'aria-label': 'primary checkbox' }}
|
||||
@@ -2251,8 +2337,8 @@ const CreateEventingStep2 = ({finished, back, first, last, cancel, prevData, ste
|
||||
<Table style={{tableLayout: "fixed", width: "100%",}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "3rem"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"} style={{width: "3rem"}}></MythicStyledTableCell>
|
||||
<MythicStyledTableCell className={"empty-table-header"}></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -2455,11 +2541,11 @@ export function CreateEventingStepper(props){
|
||||
const getStepContent = (step) => {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return <CreateEventingStep1 prevData={payload[0]} finished={handleStepData} back={cancelStep} first={true} last={false} cancel={props.onClose}/>;
|
||||
return <CreateEventingStep1 prevData={payload[0]} finished={handleStepData} back={cancelStep} first={true} last={false} cancel={() => props.onClose(null, true)}/>;
|
||||
case 1:
|
||||
return <CreateEventingStep2 prevData={payload[1]} step1Data={payload[0]} finished={handleStepData} back={cancelStep} first={false} last={false} cancel={props.onClose}/>;
|
||||
return <CreateEventingStep2 prevData={payload[1]} step1Data={payload[0]} finished={handleStepData} back={cancelStep} first={false} last={false} cancel={() => props.onClose(null, true)}/>;
|
||||
case 2:
|
||||
return <CreateEventingStep3 prevData={payload[2]} step1Data={payload[0]} step2Data={payload[1]} finished={finished} back={cancelStep} first={false} last={true} cancel={props.onClose}/>;
|
||||
return <CreateEventingStep3 prevData={payload[2]} step1Data={payload[0]} step2Data={payload[1]} finished={finished} back={cancelStep} first={false} last={true} cancel={() => props.onClose(null, true)}/>;
|
||||
default:
|
||||
return "unknown step";
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {ResponseDisplayPlaintext} from "../Callbacks/ResponseDisplayPlaintext";
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import {UploadEventFile} from "../../MythicComponents/MythicFileUpload";
|
||||
|
||||
const updateEventGroupMutation = gql(`
|
||||
mutation updateEventGroupMutation($eventgroup_id: Int!, $updated_config: String) {
|
||||
@@ -28,7 +29,7 @@ query exportWorkflow($eventgroup_id: Int!, $include_steps: Boolean!, $output_for
|
||||
}
|
||||
`)
|
||||
const outputFormatOptions = ["yaml", "json", "toml"];
|
||||
export function EventGroupTableEditDialog({onClose, selectedEventGroup}) {
|
||||
export function EventGroupTableEditDialog({onClose, selectedEventGroup, includeSteps = false}) {
|
||||
const workflowRef = React.useRef("");
|
||||
const [outputFormat, setOutputFormat] = React.useState("yaml");
|
||||
const [fetchedData, setFetchedData] = React.useState(false);
|
||||
@@ -47,6 +48,18 @@ export function EventGroupTableEditDialog({onClose, selectedEventGroup}) {
|
||||
snackActions.error("Failed to update");
|
||||
}
|
||||
});
|
||||
const submitAsFile = async (evt) => {
|
||||
let blob = new Blob([workflowRef.current], { type: 'text/plain' });
|
||||
let file = new File([blob], "manual_eventing.yaml", {type: "text/plain"});
|
||||
let uploadStatus = await UploadEventFile(file, "New Manual Eventing Workflow");
|
||||
if(!uploadStatus){
|
||||
snackActions.error("Failed to upload file");
|
||||
}
|
||||
if(uploadStatus.status === "error"){
|
||||
snackActions.error(uploadStatus.error);
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
const [getExportedWorkflow] = useLazyQuery(getExportWorkflow, {
|
||||
fetchPolicy: "no-cache",
|
||||
onCompleted: (data) => {
|
||||
@@ -66,9 +79,9 @@ export function EventGroupTableEditDialog({onClose, selectedEventGroup}) {
|
||||
React.useEffect( () => {
|
||||
getExportedWorkflow({variables: {
|
||||
eventgroup_id: selectedEventGroup.id,
|
||||
include_steps: false,
|
||||
include_steps: includeSteps,
|
||||
output_format: outputFormat}})
|
||||
}, [outputFormat, selectedEventGroup.id]);
|
||||
}, [outputFormat, selectedEventGroup.id, includeSteps]);
|
||||
const onUpdateClick = () => {
|
||||
UpdateEventGroupMutation({variables: {eventgroup_id: selectedEventGroup.id, updated_config: workflowRef.current}})
|
||||
}
|
||||
@@ -104,8 +117,8 @@ export function EventGroupTableEditDialog({onClose, selectedEventGroup}) {
|
||||
<Button onClick={onClose} variant="contained" color="primary">
|
||||
Close
|
||||
</Button>
|
||||
<Button onClick={onUpdateClick} variant="contained" color="success">
|
||||
Update
|
||||
<Button onClick={includeSteps ? submitAsFile : onUpdateClick} variant="contained" color="success">
|
||||
{includeSteps ? "Create New Workflow": "Update"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</React.Fragment>
|
||||
|
||||
@@ -13,13 +13,12 @@ import Moment from 'react-moment';
|
||||
import moment from 'moment';
|
||||
import CastConnectedTwoToneIcon from '@mui/icons-material/CastConnectedTwoTone';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import {MythicDialog} from "../../MythicComponents/MythicDialog";
|
||||
import {MythicDialog, MythicModifyStringDialog} from "../../MythicComponents/MythicDialog";
|
||||
import OpenInNewTwoToneIcon from '@mui/icons-material/OpenInNewTwoTone';
|
||||
import IosShareIcon from '@mui/icons-material/IosShare';
|
||||
import {copyStringToClipboard} from "../../utilities/Clipboard";
|
||||
import {ipCompare} from "../Callbacks/CallbacksTable";
|
||||
import MythicResizableGrid from "../../MythicComponents/MythicResizableGrid";
|
||||
import {TableFilterDialog} from "../Callbacks/TableFilterDialog";
|
||||
import {CallbacksTableStringCell} from "../Callbacks/CallbacksTableRow";
|
||||
|
||||
const cancelEventGroupInstanceMutation = gql(`
|
||||
@@ -171,15 +170,25 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
|
||||
}
|
||||
|
||||
}, [eventgroups]);
|
||||
const columns = [
|
||||
{key: "id", type: "number", name: "ID", width: 60, enableHiding: false, disableSort: true, disableFilter: true},
|
||||
{key: "status", type: 'string', name: "Status", width: 150, disableSort: true, enableHiding: false},
|
||||
{key: "event_group", type: 'string', name: "Event Group", inMetadata: true, fillWidth: true, disableSort: true, enableHiding: false},
|
||||
{key: "trigger", type: 'string', name: "Trigger", fillWidth: true, disableSort: true, enableHiding: false},
|
||||
{key: "time", type: 'date', name: "Time", width: 300, disableSort: true,},
|
||||
{key: "operator", type: 'string', name: "Operator", inMetadata: true, fillWidth: true, disableSort: true,},
|
||||
{key: "cancel", type: 'string', name: "Action", width: 70, disableSort: true, disableFilter: true},
|
||||
];
|
||||
const columns = React.useMemo(
|
||||
() =>
|
||||
[
|
||||
{key: "id", type: "number", name: "ID", width: 60, enableHiding: false, disableSort: false, disableFilter: true},
|
||||
{key: "status", type: 'string', name: "Status", width: 150, disableSort: true, enableHiding: false},
|
||||
{key: "eventgroup", type: 'string', name: "Event Group", inMetadata: true, fillWidth: true, disableSort: false, enableHiding: false},
|
||||
{key: "trigger", type: 'string', name: "Trigger", fillWidth: true, disableSort: false, enableHiding: false},
|
||||
{key: "time", type: 'date', name: "Time", width: 300, disableSort: true,},
|
||||
{key: "operator", type: 'string', name: "Operator", inMetadata: true, fillWidth: true, disableSort: true,},
|
||||
{key: "cancel", type: 'string', name: "Action", width: 70, disableSort: true, disableFilter: true},
|
||||
]?.reduce( (prev, cur) => {
|
||||
if(filterOptions[cur.key] && String(filterOptions[cur.key]).length > 0){
|
||||
return [...prev, {...cur, filtered: true}];
|
||||
}else{
|
||||
return [...prev, {...cur}];
|
||||
}
|
||||
}, []) || []
|
||||
, [filterOptions]
|
||||
);
|
||||
const onClickHeader = (e, columnIndex) => {
|
||||
const column = columns[columnIndex];
|
||||
if(column.disableSort){
|
||||
@@ -219,7 +228,11 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
|
||||
if (sortData.sortType === 'number' || sortData.sortType === 'size' || sortData.sortType === 'date') {
|
||||
tempData.sort((a, b) => (parseInt(a[sortData.sortKey]) > parseInt(b[sortData.sortKey]) ? 1 : -1));
|
||||
} else if (sortData.sortType === 'string') {
|
||||
tempData.sort((a, b) => (a[sortData.sortKey].toLowerCase() > b[sortData.sortKey].toLowerCase() ? 1 : -1));
|
||||
if(sortData.sortKey === "eventgroup"){
|
||||
tempData.sort((a, b) => (a[sortData.sortKey].name.toLowerCase() > b[sortData.sortKey].name.toLowerCase() ? 1 : -1));
|
||||
} else {
|
||||
tempData.sort((a, b) => (a[sortData.sortKey].toLowerCase() > b[sortData.sortKey].toLowerCase() ? 1 : -1));
|
||||
}
|
||||
} else if (sortData.sortType === "ip") {
|
||||
tempData.sort((a, b) => (ipCompare(a[sortData.sortKey], b[sortData.sortKey])));
|
||||
} else if (sortData.sortType === "array") {
|
||||
@@ -364,11 +377,11 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
|
||||
const sortColumn = columns.findIndex((column) => column.key === sortData.sortKey);
|
||||
const [openContextMenu, setOpenContextMenu] = React.useState(false);
|
||||
const [selectedColumn, setSelectedColumn] = React.useState({});
|
||||
const onSubmitFilterOptions = (newFilterOptions) => {
|
||||
setFilterOptions(newFilterOptions);
|
||||
const onSubmitFilterOptions = (value) => {
|
||||
setFilterOptions({...filterOptions, [selectedColumn.key]: value });
|
||||
}
|
||||
const contextMenuOptions = [{
|
||||
name: 'Filter Column',
|
||||
name: 'Filter Column', type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -402,13 +415,18 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
|
||||
onRowContextMenuClick={onRowContextClick}
|
||||
/>
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xs" open={openContextMenu}
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={<TableFilterDialog
|
||||
selectedColumn={selectedColumn}
|
||||
filterOptions={filterOptions}
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
onClose={()=>{setOpenContextMenu(false);}} />}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={filterOptions[selectedColumn.key]}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
{openDeleteDialog &&
|
||||
|
||||
@@ -17,8 +17,7 @@ import {
|
||||
} from "./EventStepRender";
|
||||
import OpenInNewTwoToneIcon from '@mui/icons-material/OpenInNewTwoTone';
|
||||
import PermMediaTwoToneIcon from '@mui/icons-material/PermMediaTwoTone';
|
||||
import {PreviewFileMediaDialog} from "../Search/PreviewFileMedia";
|
||||
import {b64DecodeUnicode} from "../Callbacks/ResponseDisplay";
|
||||
import AddCircleIcon from '@mui/icons-material/AddCircle';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
@@ -34,7 +33,7 @@ import PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwo
|
||||
import RuleTwoToneIcon from '@mui/icons-material/RuleTwoTone';
|
||||
import ChecklistRtlTwoToneIcon from '@mui/icons-material/ChecklistRtlTwoTone';
|
||||
import {EventGroupTableRunAsDialog} from "./EventApprovalDialog";
|
||||
import AttachmentIcon from '@mui/icons-material/Attachment';
|
||||
import AttachFileIcon from '@mui/icons-material/AttachFile';
|
||||
import Badge from '@mui/material/Badge';
|
||||
import {EventFileManageDialog} from "./EventFileManageDialog";
|
||||
import SpellcheckIcon from '@mui/icons-material/Spellcheck';
|
||||
@@ -67,7 +66,7 @@ mutation eventingManualTrigger($eventgroup_id: Int!){
|
||||
}
|
||||
}
|
||||
`)
|
||||
export function EventGroupTable({selectedEventGroup, me}) {
|
||||
export function EventGroupTable({selectedEventGroup, me, showInstances, showGraph, height}) {
|
||||
const [openEventStepRender, setOpenEventStepRender] = React.useState(false);
|
||||
const [openEnvView, setOpenEnvView] = React.useState(false);
|
||||
const [openTriggerDataView, setOpenTriggerDataView] = React.useState(false);
|
||||
@@ -142,7 +141,7 @@ export function EventGroupTable({selectedEventGroup, me}) {
|
||||
triggerManually({variables: {eventgroup_id: selectedEventGroup.id}});
|
||||
}
|
||||
return (
|
||||
<div style={{marginLeft: "5px", display: "flex", overflowY: "auto", flexDirection: "column", height: "100%"}}>
|
||||
<div style={{marginLeft: "5px", display: "flex", overflowY: "auto", flexDirection: "column", height: height || "100%"}}>
|
||||
|
||||
{selectedEventGroup.id === 0 &&
|
||||
<Typography variant={"h4"}><strong>All Eventing Runs</strong></Typography>
|
||||
@@ -302,12 +301,12 @@ export function EventGroupTable({selectedEventGroup, me}) {
|
||||
<OpenInNewTwoToneIcon color={"info"} style={{cursor: "pointer", marginRight: "5px"}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"View Original File"}>
|
||||
<MythicStyledTooltip title={"Create New Workflow From This One"}>
|
||||
<IconButton onClick={() => {
|
||||
setOpenFileView(true);
|
||||
}} size={"small"} color={"secondary"} style={{}}>
|
||||
<PermMediaTwoToneIcon color={"info"} style={{cursor: "pointer", marginRight: "5px"}}/>
|
||||
|
||||
<AddCircleIcon color={"success"} style={{marginLeft: "-8px", marginTop: "7px", backgroundColor: "white", borderRadius: "10px"}} fontSize={"small"} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Manage Associated Files"}>
|
||||
@@ -315,7 +314,7 @@ export function EventGroupTable({selectedEventGroup, me}) {
|
||||
setOpenFileManageView(true);
|
||||
}} size={"small"} variant={"outlined"} color={"secondary"} style={{}}>
|
||||
<Badge badgeContent={selectedEventGroup.filemeta.length} color="secondary">
|
||||
<AttachmentIcon color={"info"} style={{cursor: "pointer", marginRight: "5px"}}/>
|
||||
<AttachFileIcon color={"info"} style={{cursor: "pointer", marginRight: "5px"}}/>
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
@@ -347,11 +346,11 @@ export function EventGroupTable({selectedEventGroup, me}) {
|
||||
|
||||
</div>
|
||||
}
|
||||
<RenderSteps selectedInstanceID={selectedInstanceID} selectedEventGroup={selectedEventGroup} />
|
||||
<EventGroupInstances setSelectedInstance={setSelectedInstanceID}
|
||||
{showGraph && <RenderSteps selectedInstanceID={selectedInstanceID} selectedEventGroup={selectedEventGroup} />}
|
||||
{showInstances && <EventGroupInstances setSelectedInstance={setSelectedInstanceID}
|
||||
selectedInstanceID={selectedInstanceID}
|
||||
foundQueryInstanceRef={foundQueryInstanceRef}
|
||||
selectedEventGroup={selectedEventGroup} me={me}/>
|
||||
selectedEventGroup={selectedEventGroup} me={me}/>}
|
||||
{openEventStepRender &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openEventStepRender}
|
||||
onClose={() => {
|
||||
@@ -374,19 +373,16 @@ export function EventGroupTable({selectedEventGroup, me}) {
|
||||
/>
|
||||
}
|
||||
{openFileView &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openFileView}
|
||||
onClose={(e) => {
|
||||
<MythicDialog fullWidth={true} maxWidth="lg" open={openFileView}
|
||||
onClose={() => {
|
||||
setOpenFileView(false);
|
||||
}}
|
||||
innerDialog={<PreviewFileMediaDialog
|
||||
agent_file_id={selectedEventGroup.filemetum.agent_file_id}
|
||||
filename={b64DecodeUnicode(selectedEventGroup.filemetum.filename_text)}
|
||||
onClose={(e) => {
|
||||
setOpenFileView(false);
|
||||
}}/>}
|
||||
innerDialog={<EventGroupTableEditDialog onClose={() => {
|
||||
setOpenFileView(false);
|
||||
}} me={me} selectedEventGroup={selectedEventGroup} includeSteps={true} />}
|
||||
/>
|
||||
}
|
||||
{openEnvView && <MythicDialog fullWidth={true} maxWidth="md" open={openEnvView}
|
||||
{openEnvView && <MythicDialog fullWidth={true} maxWidth="lg" open={openEnvView}
|
||||
onClose={() => {
|
||||
setOpenEnvView(false);
|
||||
}}
|
||||
@@ -398,7 +394,7 @@ export function EventGroupTable({selectedEventGroup, me}) {
|
||||
}}/>}
|
||||
/>
|
||||
}
|
||||
{openTriggerDataView && <MythicDialog fullWidth={true} maxWidth="md" open={openTriggerDataView}
|
||||
{openTriggerDataView && <MythicDialog fullWidth={true} maxWidth="lg" open={openTriggerDataView}
|
||||
onClose={() => {
|
||||
setOpenTriggerDataView(false);
|
||||
}}
|
||||
@@ -412,7 +408,7 @@ export function EventGroupTable({selectedEventGroup, me}) {
|
||||
/>
|
||||
}
|
||||
{openFileManageView &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openFileManageView}
|
||||
<MythicDialog fullWidth={true} maxWidth="lg" open={openFileManageView}
|
||||
onClose={() => {
|
||||
setOpenFileManageView(false);
|
||||
}}
|
||||
@@ -432,7 +428,7 @@ export function EventGroupTable({selectedEventGroup, me}) {
|
||||
/>
|
||||
}
|
||||
{openConsumingContainerDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openConsumingContainerDialog}
|
||||
<MythicDialog fullWidth={true} maxWidth="lg" open={openConsumingContainerDialog}
|
||||
onClose={() => {
|
||||
setOpenConsumingContainerDialog(false);
|
||||
}}
|
||||
|
||||
@@ -614,6 +614,7 @@ function EventStepRender({selectedEventGroup, useSuppliedData}) {
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
nodeTypes={nodeTypes}
|
||||
onNodeClick={onPaneClick}
|
||||
onPaneClick={onPaneClick}
|
||||
onNodeContextMenu={onNodeContextMenu}
|
||||
>
|
||||
@@ -622,7 +623,7 @@ function EventStepRender({selectedEventGroup, useSuppliedData}) {
|
||||
EventGroup: {selectedEventGroup.id}
|
||||
</Typography>
|
||||
}
|
||||
<Controls showInteractive={false}>
|
||||
<Controls showInteractive={false} style={{color: "black"}}>
|
||||
</Controls>
|
||||
</ReactFlow>
|
||||
{openContextMenu &&
|
||||
@@ -905,13 +906,14 @@ function EventStepInstanceRender({selectedEventGroupInstance}) {
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
nodeTypes={nodeTypes}
|
||||
onNodeClick={onPaneClick}
|
||||
onPaneClick={onPaneClick}
|
||||
onNodeContextMenu={onNodeContextMenu}
|
||||
>
|
||||
<Typography component={"h1"} style={{marginLeft: "10px"}}>
|
||||
Instance: {selectedEventGroupInstance}
|
||||
</Typography>
|
||||
<Controls showInteractive={false}>
|
||||
<Controls showInteractive={false} style={{color: "black"}}>
|
||||
</Controls>
|
||||
</ReactFlow>
|
||||
{openContextMenu &&
|
||||
@@ -1089,10 +1091,10 @@ function EventStepInstanceDetailDialog({selectedEventStepInstance, onClose}) {
|
||||
</div>
|
||||
|
||||
</DialogTitle>
|
||||
<DialogContent dividers={true} style={{height: "calc(75vh)"}}>
|
||||
<Typography style={{width: "100%", padding: "10px",
|
||||
<DialogContent dividers={true} style={{height: "calc(75vh)", padding: "5px"}}>
|
||||
<Typography style={{width: "100%", padding: "0px", paddingLeft: "10px",
|
||||
backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main}}
|
||||
variant={'h5'} component={Paper} >
|
||||
variant={'h6'} component={Paper} >
|
||||
Action: <b>{data.eventstepinstance_by_pk.eventstep.action}</b>
|
||||
</Typography>
|
||||
<Paper style={{marginTop: "5px"}}>
|
||||
@@ -1100,10 +1102,8 @@ function EventStepInstanceDetailDialog({selectedEventStepInstance, onClose}) {
|
||||
TransitionProps={{ unmountOnExit: true }} defaultExpanded={false}
|
||||
onChange={() => {setExpandStepTable(!expandStepTable)}} expanded={expandStepTable}
|
||||
>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls={`panel1c-stdout`}
|
||||
>Original and Instance Metadata
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />} aria-controls={`panel1c-stdout`}>
|
||||
Original and Instance Metadata
|
||||
</AccordionSummary>
|
||||
<AccordionDetails style={{cursor: "default"}}>
|
||||
<Table style={{width: "100%", overflow: "auto", tableLayout: "fixed", }}>
|
||||
@@ -1170,6 +1170,7 @@ function EventStepInstanceDetailDialog({selectedEventStepInstance, onClose}) {
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Paper>
|
||||
{ data.eventstepinstance_by_pk.stdout !== "" &&
|
||||
<Paper style={{marginTop: "5px"}}>
|
||||
<Accordion TransitionProps={{unmountOnExit: true}} defaultExpanded={false}
|
||||
onChange={() => {
|
||||
@@ -1201,6 +1202,8 @@ function EventStepInstanceDetailDialog({selectedEventStepInstance, onClose}) {
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Paper>
|
||||
}
|
||||
{data.eventstepinstance_by_pk.stderr !== "" &&
|
||||
<Paper style={{marginTop: "5px"}}>
|
||||
<Accordion TransitionProps={{ unmountOnExit: true }} defaultExpanded={false}
|
||||
onChange={() => {setExpandStderr(!expandStderr)}} expanded={expandStderr}
|
||||
@@ -1230,7 +1233,7 @@ function EventStepInstanceDetailDialog({selectedEventStepInstance, onClose}) {
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Paper>
|
||||
|
||||
}
|
||||
{data.eventstepinstance_by_pk.callbacks.length > 0 &&
|
||||
<>
|
||||
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
|
||||
@@ -1583,9 +1586,8 @@ function EventDetailsPayloadsTable({payloads, deletePayload}){
|
||||
snackActions.success(<SnackMessage
|
||||
file_id={data.download_bulk.file_id}
|
||||
/>, {toastId: data.download_bulk.file_id, autoClose: false, closeOnClick: false});
|
||||
//snackActions.success("", {persist: true, content: key => <MythicSnackDownload id={key} title="Download Zip File" innerText="Filenames are random UUIDs, so a JSON file is included with a mapping of UUID to real filename" downloadLink={window.location.origin + "/api/v1.4/files/download/" + data.download_bulk.file_id} />});
|
||||
}else{
|
||||
snackActions.error(data.error);
|
||||
snackActions.error(data.download_bulk.error);
|
||||
}
|
||||
},
|
||||
onError: (data) => {
|
||||
@@ -1723,9 +1725,8 @@ function EventDetailsFilesTable({files}){
|
||||
snackActions.success(<SnackMessage
|
||||
file_id={data.download_bulk.file_id}
|
||||
/>, {toastId: data.download_bulk.file_id, autoClose: false, closeOnClick: false});
|
||||
//snackActions.success("", {persist: true, content: key => <MythicSnackDownload id={key} title="Download Zip File" innerText="Filenames are random UUIDs, so a JSON file is included with a mapping of UUID to real filename" downloadLink={window.location.origin + "/api/v1.4/files/download/" + data.download_bulk.file_id} />});
|
||||
}else{
|
||||
snackActions.error(data.error);
|
||||
snackActions.error(data.download_bulk.error);
|
||||
}
|
||||
},
|
||||
onError: (data) => {
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
import React, {useContext} from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import {useMutation,useQuery, gql} from '@apollo/client';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import {IconButton} from '@mui/material';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Select from '@mui/material/Select';
|
||||
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import {TextField} from '@mui/material';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import {MeContext} from "../../App";
|
||||
import {EventGroupTable} from "./EventGroupTable";
|
||||
import {MythicDraggableDialogTitle} from "../../MythicComponents/MythicDraggableDialogTitle";
|
||||
|
||||
const triggerManualMutation = gql(`
|
||||
mutation triggerManualMutation($eventgroup_id: Int!, $env_data: jsonb){
|
||||
eventingTriggerManual(eventgroup_id: $eventgroup_id, env_data: $env_data){
|
||||
status
|
||||
error
|
||||
}
|
||||
}
|
||||
`);
|
||||
const triggerManualBulkMutation = gql(`
|
||||
mutation triggerManualBulkMutation($eventgroup_id: Int!, $env_data: jsonb, $trigger_context_type: String!, $trigger_context_ids: [Int!]!){
|
||||
eventingTriggerManualBulk(eventgroup_id: $eventgroup_id, env_data: $env_data, trigger_context_type: $trigger_context_type, trigger_context_ids: $trigger_context_ids){
|
||||
status
|
||||
error
|
||||
}
|
||||
}
|
||||
`);
|
||||
const getActiveWorkflows = gql(`
|
||||
query getActiveWorkflows {
|
||||
eventgroup(where: {deleted: {_eq: false}, active: {_eq: true}, approved_to_run: {_eq: true}}, order_by: {id: desc}){
|
||||
id
|
||||
operator {
|
||||
username
|
||||
}
|
||||
filemetum {
|
||||
agent_file_id
|
||||
id
|
||||
filename_text
|
||||
}
|
||||
filemeta(where: {deleted: {_eq: false}}) {
|
||||
agent_file_id
|
||||
id
|
||||
filename_text
|
||||
deleted
|
||||
}
|
||||
name
|
||||
description
|
||||
trigger
|
||||
trigger_data
|
||||
next_scheduled_run
|
||||
keywords
|
||||
environment
|
||||
active
|
||||
deleted
|
||||
created_at
|
||||
run_as
|
||||
approved_to_run
|
||||
eventgroupapprovals(order_by: {id: asc}) {
|
||||
id
|
||||
operator {
|
||||
id
|
||||
username
|
||||
}
|
||||
approved
|
||||
created_at
|
||||
updated_at
|
||||
}
|
||||
eventgroupconsumingcontainers {
|
||||
id
|
||||
consuming_container_name
|
||||
all_functions_available
|
||||
function_names
|
||||
consuming_container {
|
||||
container_running
|
||||
subscriptions
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
export function EventTriggerContextSelectDialog({onClose, triggerContext}) {
|
||||
const me = useContext(MeContext);
|
||||
const [workflowOptions, setWorkflowOptions] = React.useState([]);
|
||||
const [selectedEventGroup, setSelectedEventGroup] = React.useState({id: 0});
|
||||
useQuery(getActiveWorkflows, {
|
||||
fetchPolicy: "no-cache",
|
||||
onCompleted: (data) => {
|
||||
setWorkflowOptions(data.eventgroup);
|
||||
if(data.eventgroup.length > 0){
|
||||
setSelectedEventGroup(data.eventgroup[0]);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log(error);
|
||||
}
|
||||
})
|
||||
const [dictionaryData, setDictionaryData] = React.useState([]);
|
||||
const typeOptions = ["text", "number"];
|
||||
const [triggerManual] = useMutation(triggerManualMutation, {
|
||||
onCompleted: (data) => {
|
||||
if(data.eventingTriggerManual.status === "success"){
|
||||
snackActions.success("Successfully initiated trigger");
|
||||
onClose();
|
||||
} else {
|
||||
snackActions.error(data.eventingTriggerManual.error);
|
||||
}
|
||||
},
|
||||
onError: (data) => {
|
||||
console.log(data);
|
||||
}
|
||||
})
|
||||
const [triggerManualBulk] = useMutation(triggerManualBulkMutation, {
|
||||
onCompleted: (data) => {
|
||||
if(data.eventingTriggerManualBulk.status === "success"){
|
||||
snackActions.success("Successfully initiated trigger");
|
||||
onClose();
|
||||
} else {
|
||||
snackActions.error(data.eventingTriggerManualBulk.error);
|
||||
}
|
||||
},
|
||||
onError: (data) => {
|
||||
console.log(data);
|
||||
}
|
||||
})
|
||||
const handleWorkflowChange = (event) => {
|
||||
setSelectedEventGroup(event.target.value);
|
||||
}
|
||||
const addRow = () => {
|
||||
setDictionaryData([...dictionaryData, {
|
||||
"type": "text",
|
||||
"key": "",
|
||||
"value": "",
|
||||
}])
|
||||
}
|
||||
const changeRowType = (event, index) => {
|
||||
let newType = event.target.value;
|
||||
const newDictionaryData = dictionaryData.map( (row, i) => {
|
||||
if(index === i){
|
||||
return {key: row["key"], type: newType, value: newType === "number" ? 0 : ""};
|
||||
}
|
||||
return {...row};
|
||||
});
|
||||
setDictionaryData(newDictionaryData);
|
||||
}
|
||||
const removeRow = (index) => {
|
||||
let newDictionaryData = [...dictionaryData];
|
||||
newDictionaryData.splice(index, 1);
|
||||
setDictionaryData(newDictionaryData);
|
||||
}
|
||||
const onChangeRowKey = (value, index) => {
|
||||
const newDictionaryData = dictionaryData.map( (e, i) => {
|
||||
if(index === i){
|
||||
return {...e, key: value};
|
||||
}
|
||||
return {...e};
|
||||
});
|
||||
setDictionaryData(newDictionaryData);
|
||||
}
|
||||
const onChangeRowValue = (value, index) => {
|
||||
const newDictionaryData = dictionaryData.map( (e, i) => {
|
||||
if(index === i){
|
||||
return {...e, value: value};
|
||||
}
|
||||
return {...e};
|
||||
});
|
||||
setDictionaryData(newDictionaryData);
|
||||
}
|
||||
const onSubmit = () => {
|
||||
let keywordEnvData = dictionaryData.reduce( (prev, cur) => {
|
||||
try{
|
||||
if(cur.type === "number"){
|
||||
prev[cur.key] = parseInt(cur.value);
|
||||
} else {
|
||||
prev[cur.key] = cur.value;
|
||||
}
|
||||
}catch(error){
|
||||
prev[cur.key] = cur.value;
|
||||
}
|
||||
return {...prev};
|
||||
}, {});
|
||||
if(triggerContext.name !== undefined){
|
||||
keywordEnvData[triggerContext.name] = triggerContext.value;
|
||||
triggerManual({variables: {eventgroup_id: selectedEventGroup.id, env_data:keywordEnvData}});
|
||||
} else if(triggerContext.trigger_context_type !== undefined){
|
||||
triggerManualBulk({variables: {eventgroup_id: selectedEventGroup.id, env_data: keywordEnvData,
|
||||
trigger_context_type: triggerContext.trigger_context_type,
|
||||
trigger_context_ids: triggerContext.trigger_context_ids}})
|
||||
} else {
|
||||
snackActions.error("No trigger context name or type, can't submit");
|
||||
}
|
||||
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicDraggableDialogTitle >Trigger a workflow</MythicDraggableDialogTitle>
|
||||
<DialogContent dividers={true} style={{maxHeight: "calc(70vh)"}}>
|
||||
<DialogContentText>
|
||||
Trigger a workflow with selected context and optional additional data.
|
||||
</DialogContentText>
|
||||
<Select
|
||||
style={{marginBottom: "10px", width: "100%"}}
|
||||
value={selectedEventGroup}
|
||||
onChange={handleWorkflowChange}
|
||||
>
|
||||
{
|
||||
workflowOptions.map((opt, i) => (
|
||||
<MenuItem key={opt.id} value={opt}>{opt.name + " - " + opt.description}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
{selectedEventGroup.id !== 0 &&
|
||||
<EventGroupTable me={me} selectedEventGroup={selectedEventGroup} showInstances={false} showGraph={false} height={"unset"}/>
|
||||
}
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{width: "50%"}}>Key</TableCell>
|
||||
<TableCell>Value</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{dictionaryData.map( (e, index) => (
|
||||
<TableRow key={"dictionarydata" + index}>
|
||||
<MythicStyledTableCell style={{display: "flex"}} >
|
||||
<IconButton color={"error"} onClick={() => removeRow(index)} >
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
<Select
|
||||
style={{}}
|
||||
value={e.type}
|
||||
onChange={(evt) => changeRowType(evt, index)}
|
||||
>
|
||||
{
|
||||
typeOptions.map((opt, i) => (
|
||||
<MenuItem key={"type" + opt} value={opt}>{opt}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
<TextField value={e.key}
|
||||
style={{width: "100%"}}
|
||||
onChange={(evt) => onChangeRowKey(evt.target.value, index)} />
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<TextField value={e.value}
|
||||
style={{width: "100%"}}
|
||||
onChange={(evt) => onChangeRowValue(evt.target.value, index)} />
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
<TableRow>
|
||||
<MythicStyledTableCell>
|
||||
<Button onClick={addRow} color={"success"}>
|
||||
Add Entry
|
||||
</Button>
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant="contained" color="primary">
|
||||
Close
|
||||
</Button>
|
||||
<Button onClick={onSubmit} variant="contained" color="success">
|
||||
Submit
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
Loaded 100 of 411 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user