mirror of
https://github.com/its-a-feature/Mythic.git
synced 2026-11-09 17:20:01 -10:00
Compare commits
230
Commits
v3.3.0.136
...
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 |
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/
|
||||
+318
@@ -4,6 +4,324 @@ 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
|
||||
|
||||
@@ -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,314 @@ 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,0 +1,26 @@
|
||||
const webpack = require('webpack');
|
||||
module.exports = function override(config, env) {
|
||||
config.resolve.fallback = {
|
||||
//url: require.resolve('url'),
|
||||
fs: false,
|
||||
assert: require.resolve('assert'),
|
||||
crypto: require.resolve('crypto-browserify'),
|
||||
path: require.resolve('path-browserify'),
|
||||
//http: require.resolve('stream-http'),
|
||||
//https: require.resolve('https-browserify'),
|
||||
//os: require.resolve('os-browserify/browser'),
|
||||
//buffer: require.resolve('buffer'),
|
||||
stream: require.resolve('stream-browserify'),
|
||||
};
|
||||
/*
|
||||
config.plugins.push(
|
||||
new webpack.ProvidePlugin({
|
||||
process: 'process/browser',
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
);
|
||||
|
||||
*/
|
||||
|
||||
return config;
|
||||
}
|
||||
Generated
+923
-277
File diff suppressed because it is too large.
Load diff
@@ -6,7 +6,11 @@
|
||||
"author": "@its_a_feature_",
|
||||
"description": "React UI for Mythic C2 Framework",
|
||||
"devDependencies": {
|
||||
"react-scripts": "^5.0.1"
|
||||
"buffer": "^6.0.3",
|
||||
"crypto-browserify": "^3.12.1",
|
||||
"react-app-rewired": "^2.2.1",
|
||||
"react-scripts": "^5.0.1",
|
||||
"stream-browserify": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.13.8",
|
||||
@@ -38,6 +42,7 @@
|
||||
"jwt-decode": "^4.0.0",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"path-browserify": "^1.0.1",
|
||||
"randexp": "^0.5.3",
|
||||
"react": "^18.3.1",
|
||||
"react-ace": "^14.0.1",
|
||||
@@ -45,7 +50,7 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-moment": "^1.1.3",
|
||||
"react-router-dom": "^7.5.2",
|
||||
"react-router-dom": "^7.13.1",
|
||||
"react-scrollbar-size": "^5.0.0",
|
||||
"react-split": "^2.0.14",
|
||||
"react-toastify": "^11.0.5",
|
||||
@@ -55,18 +60,19 @@
|
||||
"react-window": "^1.8.11",
|
||||
"save-svg-as-png": "^1.4.17",
|
||||
"semver": "^7.6.2",
|
||||
"sql.js": "^1.13.0",
|
||||
"styled-components": "^6.1.17",
|
||||
"uuid": "^9.0.1",
|
||||
"webpack": "^5.99.6",
|
||||
"webpack": "^5.105.0",
|
||||
"word-wrap": "^1.2.5"
|
||||
},
|
||||
"scripts": {
|
||||
"react-start": "CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=true WDS_POLLING=true BROWSER=none PORT=3000 WDS_SOCKET_PORT=7443 WDS_SOCKET_PATH=ws react-scripts start",
|
||||
"react-build": "react-scripts build",
|
||||
"react-test": "react-scripts test",
|
||||
"react-start": "CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=true WDS_POLLING=true BROWSER=none PORT=3000 WDS_SOCKET_PORT=7443 WDS_SOCKET_PATH=ws react-app-rewired start",
|
||||
"react-build": "react-app-rewired build",
|
||||
"react-test": "react-app-rewired test",
|
||||
"react-eject": "react-scripts eject",
|
||||
"build": "npm run react-build",
|
||||
"start": "cross-env BROWSER=none CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=true WDS_POLLING=true PORT=3000 WDS_SOCKET_PORT=7443 WDS_SOCKET_PATH=ws npm run react-start"
|
||||
"start": "BROWSER=none CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=true WDS_POLLING=true PORT=3000 WDS_SOCKET_PORT=7443 WDS_SOCKET_PATH=ws npm run react-start"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
|
||||
+33
-10
@@ -19,6 +19,11 @@ export const taskTimestampDisplayFieldOptions = [
|
||||
}
|
||||
]
|
||||
export const taskingContextFieldsOptions = ["impersonation_context", "cwd", "user", "host", "ip", "pid", "process_short_name", "extra_info", "architecture"].sort();
|
||||
export const defaultShortcuts = [
|
||||
"ActiveCallbacks", "Payloads", "PayloadTypesAndC2",
|
||||
"Operations", "SearchFiles", "SearchProxies",
|
||||
"CreatePayload", "Eventing",
|
||||
].sort();
|
||||
export const operatorSettingDefaults = {
|
||||
fontSize: 12,
|
||||
navBarOpen: false,
|
||||
@@ -27,17 +32,19 @@ 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,
|
||||
hideTaskingContext: false,
|
||||
taskingContextFields: ["impersonation_context", "cwd", "extra_info"],
|
||||
["experiment-responseStreamLimit"]: 50,
|
||||
taskingContextFields: ["impersonation_context", "cwd"],
|
||||
["experiment-responseStreamLimit"]: 200,
|
||||
sideShortcuts: defaultShortcuts,
|
||||
palette: {
|
||||
primary: {
|
||||
dark: "#75859b",
|
||||
@@ -108,8 +115,12 @@ export const operatorSettingDefaults = {
|
||||
light: '#ffffff'
|
||||
},
|
||||
navBarColor: {
|
||||
dark: "#2e373c",
|
||||
light: "#3c4d67",
|
||||
dark: "#194573",
|
||||
light: "#3b606d",
|
||||
},
|
||||
navBarBottomColor: {
|
||||
dark: "#330814",
|
||||
light: "#283581",
|
||||
},
|
||||
taskPromptTextColor: {
|
||||
dark: '#bebebe',
|
||||
@@ -131,13 +142,25 @@ export const operatorSettingDefaults = {
|
||||
dark: "#2a5953",
|
||||
light: "#a7ce9d",
|
||||
},
|
||||
emptyFolderColor: {
|
||||
dark: '#bebebe',
|
||||
light: '#a6a5a5'
|
||||
},
|
||||
outputBackgroundColor: {
|
||||
dark: '#282828',
|
||||
light: '#f6f6f6'
|
||||
},
|
||||
outputTextColor: {
|
||||
dark: '#f6f6f6',
|
||||
light: '#282828',
|
||||
},
|
||||
borderColor: {
|
||||
dark: "#595858",
|
||||
light: "#c5c5c5"
|
||||
}
|
||||
},
|
||||
}
|
||||
export const defaultShortcuts = [
|
||||
"ActiveCallbacks", "Payloads", "PayloadTypesAndC2",
|
||||
"Operations", "SearchFiles", "SearchProxies",
|
||||
"Reporting", "Eventing",
|
||||
].sort();
|
||||
|
||||
export const mePreferences = makeVar(operatorSettingDefaults);
|
||||
|
||||
|
||||
|
||||
@@ -38,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 {
|
||||
@@ -129,10 +142,12 @@ export function App(props) {
|
||||
pageHeaderText: {
|
||||
main: 'white',
|
||||
},
|
||||
topAppBarColor: themeMode === 'dark' ? preferences?.palette?.navBarColor?.dark || operatorSettingDefaults.palette.navBarColor.dark :
|
||||
preferences?.palette?.navBarColor?.light || operatorSettingDefaults.palette.navBarColor.light,
|
||||
topAppBarColor: themeMode === 'dark' ? (preferences?.palette?.navBarColor?.dark || operatorSettingDefaults.palette.navBarColor.dark) :
|
||||
(preferences?.palette?.navBarColor?.light || operatorSettingDefaults.palette.navBarColor.light),
|
||||
topAppBarBottomColor: themeMode === 'dark' ? preferences?.palette?.navBarBottomColor?.dark || operatorSettingDefaults.palette.navBarBottomColor.dark :
|
||||
preferences?.palette?.navBarBottomColor?.light || operatorSettingDefaults.palette.navBarBottomColor.light,
|
||||
typography: {
|
||||
fontSize: preferences?.fontSize,
|
||||
fontSize: 12, //preferences?.fontSize,
|
||||
fontFamily: preferences?.fontFamily
|
||||
},
|
||||
taskPromptTextColor: themeMode === 'dark' ? preferences?.palette?.taskPromptTextColor?.dark || operatorSettingDefaults.palette.taskPromptTextColor.dark :
|
||||
@@ -145,6 +160,14 @@ export function App(props) {
|
||||
preferences?.palette?.taskContextImpersonationColor?.light || operatorSettingDefaults.palette.taskContextImpersonationColor.light,
|
||||
taskContextExtraColor: themeMode === 'dark' ? preferences?.palette?.taskContextExtraColor?.dark || operatorSettingDefaults.palette.taskContextExtraColor.dark :
|
||||
preferences?.palette?.taskContextExtraColor?.light || operatorSettingDefaults.palette.taskContextExtraColor.light,
|
||||
emptyFolderColor: themeMode === 'dark' ? preferences?.palette?.emptyFolderColor?.dark || operatorSettingDefaults.palette.emptyFolderColor.dark :
|
||||
preferences?.palette?.emptyFolderColor?.light || operatorSettingDefaults.palette.emptyFolderColor.light,
|
||||
outputBackgroundColor: themeMode === 'dark' ? preferences?.palette?.outputBackgroundColor?.dark || operatorSettingDefaults.palette.outputBackgroundColor.dark :
|
||||
preferences?.palette?.outputBackgroundColor?.light || operatorSettingDefaults.palette.outputBackgroundColor.light,
|
||||
outputTextColor: themeMode === 'dark' ? preferences?.palette?.outputTextColor?.dark || operatorSettingDefaults.palette.outputTextColor.dark :
|
||||
preferences?.palette?.outputTextColor?.light || operatorSettingDefaults.palette.outputTextColor.light,
|
||||
borderColor: themeMode === 'dark' ? preferences?.palette?.borderColor?.dark || operatorSettingDefaults.palette.borderColor.dark :
|
||||
preferences?.palette?.borderColor?.light || operatorSettingDefaults.palette.borderColor.light,
|
||||
})
|
||||
}catch(error){
|
||||
console.log(error);
|
||||
@@ -222,8 +245,18 @@ export function App(props) {
|
||||
},
|
||||
topAppBarColor: themeMode === 'dark' ? operatorSettingDefaults.palette.navBarColor.dark :
|
||||
operatorSettingDefaults.palette.navBarColor.light,
|
||||
topAppBarBottomColor: themeMode === 'dark' ? operatorSettingDefaults.palette.navBarBottomColor.dark :
|
||||
operatorSettingDefaults.palette.navBarBottomColor.light,
|
||||
emptyFolderColor: themeMode === 'dark' ? operatorSettingDefaults.palette.emptyFolderColor.dark :
|
||||
operatorSettingDefaults.palette.emptyFolderColor.light,
|
||||
outputBackgroundColor: themeMode === 'dark' ? operatorSettingDefaults.palette.outputBackgroundColor.dark :
|
||||
operatorSettingDefaults.palette.outputBackgroundColor.light,
|
||||
outputTextColor: themeMode === 'dark' ? operatorSettingDefaults.palette.outputTextColor.dark :
|
||||
operatorSettingDefaults.palette.outputTextColor.light,
|
||||
borderColor: themeMode === 'dark' ? operatorSettingDefaults.palette.borderColor.dark :
|
||||
operatorSettingDefaults.palette.borderColor.light,
|
||||
typography: {
|
||||
fontSize: operatorSettingDefaults?.fontSize,
|
||||
fontSize: 12,//operatorSettingDefaults?.fontSize,
|
||||
fontFamily: operatorSettingDefaults?.fontFamily
|
||||
},
|
||||
})
|
||||
@@ -295,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);}}
|
||||
@@ -314,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,
|
||||
@@ -352,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}/>}/>
|
||||
@@ -406,11 +441,13 @@ export function App(props) {
|
||||
element={<LoggedInRoute me={me}><Tags me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/eventing'
|
||||
element={<LoggedInRoute me={me}><Eventing me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/jupyter' element={<LoggedInRoute me={me}><Jupyter/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/hasura' element={<LoggedInRoute me={me}><Hasura/></LoggedInRoute>}/>
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</MeContext.Provider>
|
||||
</ThemeProvider>
|
||||
</StyledEngineProvider>
|
||||
);
|
||||
|
||||
@@ -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,37 +161,48 @@ export function MythicModifyStringDialog(props) {
|
||||
}, [props.value]);
|
||||
return (
|
||||
<React.Fragment>
|
||||
{props.title !== "" && <DialogTitle id="form-dialog-title">{props.title}</DialogTitle>}
|
||||
<DialogContent dividers={true} style={{height: "100%", margin: 0, padding: 0}}>
|
||||
{props.title !== "" &&
|
||||
<MythicDraggableDialogTitle>{props.title}
|
||||
<MythicStyledTooltip title={wrap ? "Toggle off word wrap" : "Toggl on word wrap"}
|
||||
tooltipStyle={{float: "right"}}>
|
||||
<IconButton onClick={() => {setWrap(!wrap)}}>
|
||||
<WrapTextIcon color={wrap ? "success" : "secondary"} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</MythicDraggableDialogTitle>
|
||||
}
|
||||
<DialogContent dividers={true} style={{ margin: 0, padding: 0}}>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
|
||||
width="100%"
|
||||
height="100%"
|
||||
fontSize={14}
|
||||
showPrintMargin={false}
|
||||
wrapEnabled={props.wrap ? props.wrap : false}
|
||||
minLines={props.maxRows ? props.maxRows : 10}
|
||||
maxLines={props.maxRows ? props.maxRows : 10}
|
||||
wrapEnabled={wrap}
|
||||
value={comment}
|
||||
focus={true}
|
||||
onChange={onChange}
|
||||
setOptions={{
|
||||
useWorker: false
|
||||
tabSize: 4,
|
||||
useWorker: false,
|
||||
showInvisibles: false,
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{props.onClose &&
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
Close
|
||||
</Button>
|
||||
}
|
||||
{props.onSubmit &&
|
||||
<Button onClick={onCommitSubmit} variant="contained" color="success">
|
||||
{props.onSubmitText ? props.onSubmitText : "Submit"}
|
||||
</Button>
|
||||
}
|
||||
</DialogActions>
|
||||
{ (props.onClose || props.onSubmit) &&
|
||||
<DialogActions>
|
||||
{props.onClose &&
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
Close
|
||||
</Button>
|
||||
}
|
||||
{props.onSubmit &&
|
||||
<Button onClick={onCommitSubmit} variant="contained" color="success">
|
||||
{props.onSubmitText ? props.onSubmitText : "Submit"}
|
||||
</Button>
|
||||
}
|
||||
</DialogActions>
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -182,7 +274,7 @@ export function MythicViewJSONAsTableDialog(props) {
|
||||
}, [props.value, props.leftColumn, props.rightColumn]);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title" style={{wordBreak: "break-all", maxWidth: "100%"}}>{props.title}</DialogTitle>
|
||||
<MythicDraggableDialogTitle style={{wordBreak: "break-all", maxWidth: "100%"}}>{props.title}</MythicDraggableDialogTitle>
|
||||
|
||||
<TableContainer className="mythicElement" style={{paddingLeft: "10px"}}>
|
||||
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
|
||||
|
||||
export const MythicDraggableDialogTitle = ({children}) => {
|
||||
return (
|
||||
<DialogTitle id="mythic-draggable-title" style={{ cursor: 'move', width: "100%" }}>
|
||||
{children}
|
||||
</DialogTitle>
|
||||
)
|
||||
}
|
||||
@@ -239,7 +239,7 @@ const VirtualTreeRow = React.memo(({
|
||||
height: "15px",
|
||||
marginLeft: '3px',
|
||||
marginRight: '5px',
|
||||
color: item?.metadata?.has_children ? theme.folderColor : theme.palette.text.secondary,
|
||||
color: item?.has_children || item.success ? theme.folderColor : theme.emptyFolderColor,
|
||||
}}
|
||||
size={"lg"}
|
||||
onClick={handleOnClickButton} />
|
||||
@@ -250,14 +250,14 @@ const VirtualTreeRow = React.memo(({
|
||||
height: "15px",
|
||||
marginLeft: '3px',
|
||||
marginRight: '5px',
|
||||
color: item?.metadata?.has_children ? theme.folderColor : theme.palette.text.secondary, }}
|
||||
color: item?.has_children || item.success ? theme.folderColor : theme.emptyFolderColor, }}
|
||||
size={"lg"}
|
||||
icon={faFolder} onClick={handleOnClickButton} />
|
||||
)}
|
||||
<Typography
|
||||
style={{
|
||||
color:
|
||||
item?.metadata?.has_children ||
|
||||
item?.has_children ||
|
||||
item.success !== null
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary,
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
}
|
||||
@@ -22,6 +22,8 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
[data, rowIndex]
|
||||
);
|
||||
const handleClick = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if(data.onRowClick){
|
||||
data.onRowClick({event, rowDataStatic: data.items[rowIndex][columnIndex]?.props?.rowData})
|
||||
}
|
||||
@@ -44,6 +46,8 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
}
|
||||
}
|
||||
const handleMenuItemClick = (event, clickOption) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
clickOption({event, columnIndex, rowIndex, data: data.items[rowIndex][columnIndex]?.props?.rowData || {}});
|
||||
setOpenContextMenu(false);
|
||||
};
|
||||
@@ -55,7 +59,8 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
const handleContextClick = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
if(item.disableFilterMenu){
|
||||
event.stopPropagation();
|
||||
if(item?.disableFilterMenu){
|
||||
return;
|
||||
}
|
||||
contextMenuLocationRef.current.x = event.clientX;
|
||||
@@ -90,14 +95,14 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
{item}
|
||||
</div>
|
||||
<ContextMenu dropdownAnchorRef={dropdownAnchorRef} contextMenuOptions={contextMenuOptions}
|
||||
disableFilterMenu={item.disableFilterMenu} openContextMenu={openContextMenu}
|
||||
disableFilterMenu={item?.disableFilterMenu} openContextMenu={openContextMenu}
|
||||
contextMenuLocationRef={contextMenuLocationRef}
|
||||
setOpenContextMenu={setOpenContextMenu} handleMenuItemClick={handleMenuItemClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const ContextMenu = ({openContextMenu, dropdownAnchorRef, contextMenuOptions, setOpenContextMenu, handleMenuItemClick, contextMenuLocationRef}) => {
|
||||
export const ContextMenu = ({openContextMenu, dropdownAnchorRef, contextMenuOptions, setOpenContextMenu, handleMenuItemClick, contextMenuLocationRef}) => {
|
||||
const handleClose = (event) => {
|
||||
//if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
|
||||
// return;
|
||||
|
||||
+4
-2
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
||||
import {classes} from './styles';
|
||||
import Draggable from 'react-draggable';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import DragHandleIcon from '@mui/icons-material/DragHandle';
|
||||
|
||||
const DraggableHandles = React.forwardRef(({ height, rowHeight, width, minColumnWidth, columnWidths, onStop }, ref) => {
|
||||
const [isDragging, setIsDragging] = useState(-1);
|
||||
@@ -40,13 +41,14 @@ const DraggableHandles = React.forwardRef(({ height, rowHeight, width, minColumn
|
||||
setIsDragging(-1);
|
||||
onStop(data.x, i);
|
||||
}}>
|
||||
<MoreVertIcon
|
||||
<DragHandleIcon
|
||||
ref={nodeRef}
|
||||
className={isDragging === i ? classes.draggableHandlesClickAreaSelected : classes.draggableHandlesClickArea}
|
||||
style={{
|
||||
left: leftOffset + columnWidths[i] - 1 - 7,
|
||||
rotate: isDragging === i ? "0deg" : "90deg",
|
||||
}}>
|
||||
</MoreVertIcon>
|
||||
</DragHandleIcon>
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -5,13 +5,7 @@ import {classes} from './styles';
|
||||
import React from 'react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faFilter} from '@fortawesome/free-solid-svg-icons';
|
||||
import Grow from '@mui/material/Grow';
|
||||
import Popper from '@mui/material/Popper';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import MenuList from '@mui/material/MenuList';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import {ContextMenu} from "./Cell";
|
||||
|
||||
const HeaderCell = ({
|
||||
onClick = () => {},
|
||||
@@ -23,9 +17,9 @@ const HeaderCell = ({
|
||||
VariableSizeGridProps: { style, rowIndex, columnIndex, data },
|
||||
}) => {
|
||||
const dropdownAnchorRef = React.useRef(null);
|
||||
const theme = useTheme();
|
||||
const item = data.items[rowIndex][columnIndex];
|
||||
const isFiltered = item?.filtered || false;
|
||||
const contextMenuLocationRef = React.useRef({x: 0, y: 0});
|
||||
const handleClick = useCallback(
|
||||
(e) => {
|
||||
onClick(e, columnIndex);
|
||||
@@ -47,22 +41,20 @@ const HeaderCell = ({
|
||||
return;
|
||||
}
|
||||
if(contextMenuOptions && contextMenuOptions.length > 0){
|
||||
|
||||
contextMenuLocationRef.current.x = event.clientX;
|
||||
contextMenuLocationRef.current.y = event.clientY;
|
||||
setOpenContextMenu(true);
|
||||
}
|
||||
},
|
||||
[contextMenuOptions, columnIndex] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
);
|
||||
const handleMenuItemClick = (event, index) => {
|
||||
contextMenuOptions[index].click({event, columnIndex});
|
||||
const handleMenuItemClick = (event, clickOption) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
clickOption({event, columnIndex, rowIndex, data: data.items[rowIndex][columnIndex]?.props?.rowData || {}});
|
||||
setOpenContextMenu(false);
|
||||
};
|
||||
const handleClose = (event) => {
|
||||
if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
setOpenContextMenu(false);
|
||||
};
|
||||
|
||||
const handleClicks = useSingleAndDoubleClick(handleClick, handleDoubleClick);
|
||||
|
||||
return (
|
||||
@@ -73,31 +65,11 @@ const HeaderCell = ({
|
||||
</Typography>
|
||||
{isFiltered && <FontAwesomeIcon icon={faFilter} />}
|
||||
{sortIndicatorIndex === columnIndex && (sortDirection === 'ASC' ? <div>↑</div> : <div>↓</div>)}
|
||||
<Popper open={openContextMenu} anchorEl={dropdownAnchorRef.current} role={undefined} transition style={{zIndex: 40}}>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
|
||||
}}
|
||||
>
|
||||
<Paper variant="outlined" style={{backgroundColor: theme.palette.mode === 'dark' ? theme.palette.primary.dark : theme.palette.primary.light}}>
|
||||
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
|
||||
<MenuList id="split-button-menu" >
|
||||
{contextMenuOptions.map((option, index) => (
|
||||
<MenuItem
|
||||
key={option.name + index}
|
||||
onClick={(event) => handleMenuItemClick(event, index)}
|
||||
>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
<ContextMenu dropdownAnchorRef={dropdownAnchorRef} contextMenuOptions={contextMenuOptions}
|
||||
disableFilterMenu={item?.disableFilterMenu} openContextMenu={openContextMenu}
|
||||
contextMenuLocationRef={contextMenuLocationRef}
|
||||
setOpenContextMenu={setOpenContextMenu} handleMenuItemClick={handleMenuItemClick}
|
||||
/>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
|
||||
+5
-2
@@ -11,7 +11,7 @@ import {GetMythicSetting, useSetMythicSetting} from "../MythicSavedUserSetting";
|
||||
|
||||
const HeaderCellContext = createContext({});
|
||||
|
||||
const MIN_COLUMN_WIDTH = 20;
|
||||
const MIN_COLUMN_WIDTH = 100;
|
||||
const MIN_FLEX_COLUMN_WIDTH = 150;
|
||||
|
||||
|
||||
@@ -195,7 +195,10 @@ const ResizableGridWrapper = ({
|
||||
return itemRow[columnIndex]?.props?.cellData.length;
|
||||
}
|
||||
if(columns[columnIndex].type === "size"){
|
||||
if(itemRow[columnIndex]?.props?.cellData){
|
||||
if(itemRow[columnIndex]?.props?.cellData !== undefined){
|
||||
if(typeof itemRow[columnIndex]?.props?.cellData === 'number'){
|
||||
return String(itemRow[columnIndex]?.props?.cellData)?.length;
|
||||
}
|
||||
return String(itemRow[columnIndex]?.props?.cellData?.plaintext)?.length;
|
||||
}
|
||||
return itemRow[columnIndex].length;
|
||||
|
||||
@@ -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,6 +61,8 @@ export function MythicTabLabel(props) {
|
||||
getCallbackData,
|
||||
onDragTab,
|
||||
selectedIndex,
|
||||
highlight,
|
||||
newDataForTab,
|
||||
...other
|
||||
} = props;
|
||||
const onClick = (e) => {
|
||||
@@ -87,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}>
|
||||
@@ -95,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>
|
||||
|
||||
@@ -29,6 +29,7 @@ import Typography from '@mui/material/Typography';
|
||||
import MythicStyledTableCell from "./MythicTableCell";
|
||||
import {meState} from "../../cache";
|
||||
import { useReactiveVar } from '@apollo/client';
|
||||
import {MythicDraggableDialogTitle} from "./MythicDraggableDialogTitle";
|
||||
|
||||
const createNewTagMutationTemplate = ({target_object}) => {
|
||||
// target_object should be something like "task_id"
|
||||
@@ -495,20 +496,17 @@ const onAcceptDelete = () => {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Edit Tags
|
||||
</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
{openNewDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="xl" open={openNewDialog}
|
||||
<MythicDraggableDialogTitle >Edit Tags </MythicDraggableDialogTitle>
|
||||
<DialogContent dividers={true} style={{width: "100%"}}>
|
||||
{openNewDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openNewDialog}
|
||||
onClose={()=>{setOpenNewDialog(false);}}
|
||||
innerDialog={<NewTagDialog me={props.me}
|
||||
target_object={props.target_object}
|
||||
target_object_id={props.target_object_id}
|
||||
onClose={()=>{setOpenNewDialog(false);}}
|
||||
onSubmit={handleNewTagCreate} />}
|
||||
/>) : null}
|
||||
<TableContainer className="mythicElement">
|
||||
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
/>}
|
||||
<Table size="small" style={{ width: "100%", overflow: "scroll"}}>
|
||||
<TableBody>
|
||||
<TableRow hover>
|
||||
<MythicStyledTableCell style={{width: "30%"}}>Select Existing Tag to Edit or Add New</MythicStyledTableCell>
|
||||
@@ -578,7 +576,6 @@ return (
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
@@ -647,10 +644,9 @@ export function NewTagDialog(props) {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Add New Tag</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
<TableContainer className="mythicElement">
|
||||
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
<MythicDraggableDialogTitle >Add New Tag</MythicDraggableDialogTitle>
|
||||
<DialogContent dividers={true} style={{width: "100%"}}>
|
||||
<Table size="small" style={{ overflow: "scroll", width: "100%"}}>
|
||||
<TableBody>
|
||||
<TableRow hover>
|
||||
<MythicStyledTableCell style={{width: "20%"}}>
|
||||
@@ -717,7 +713,6 @@ export function NewTagDialog(props) {
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
|
||||
@@ -78,6 +78,8 @@ import {
|
||||
} from "@hello-pangea/dnd";
|
||||
import {reorder} from "./MythicComponents/MythicDraggableList";
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import LogoutIcon from '@mui/icons-material/Logout';
|
||||
import TuneIcon from '@mui/icons-material/Tune';
|
||||
|
||||
const PREFIX = 'TopAppBarVertical';
|
||||
|
||||
@@ -90,12 +92,14 @@ const openedMixin = (theme) => ({
|
||||
overflowX: 'hidden',
|
||||
borderRadius: "0 !important",
|
||||
border: "0px !important",
|
||||
backgroundColor: "unset !important",
|
||||
});
|
||||
const closedMixin = (theme) => ({
|
||||
overflowX: 'hidden',
|
||||
width: `calc(${theme.spacing(4)} + 1px)`,
|
||||
borderRadius: "0 !important",
|
||||
border: "0px !important",
|
||||
border: "0px !important",
|
||||
backgroundColor: "unset !important",
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
width: `calc(${theme.spacing(5)} + 1px)`,
|
||||
borderRadius: "0 !important",
|
||||
@@ -108,28 +112,44 @@ const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open'
|
||||
flexShrink: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
boxSizing: 'border-box',
|
||||
background: `linear-gradient(180deg, ${theme.topAppBarColor}, ${theme.topAppBarBottomColor}) !important`,
|
||||
backgroundColor: `unset !important`,
|
||||
[`& .${classes.listSubHeader}`]: {
|
||||
//color: ` ${theme.navBarTextIconColor} !important`,
|
||||
backgroundColor: `${theme.topAppBarColor} !important`,
|
||||
//backgroundColor: `${theme.topAppBarColor} !important`,
|
||||
backgroundColor: "unset !important",
|
||||
},
|
||||
[`& .${classes.listSubHeader}:hover`]: {
|
||||
backgroundColor: `${theme.topAppBarColor} !important`,
|
||||
//backgroundColor: `${theme.topAppBarColor} !important`,
|
||||
filter: `brightness(90%)`,
|
||||
color: `${theme.navBarTextIconColor} !important`,
|
||||
backgroundColor: "unset !important",
|
||||
},
|
||||
variants: [
|
||||
{
|
||||
props: ({ open }) => open,
|
||||
style: {
|
||||
...openedMixin(theme),
|
||||
'& .MuiDrawer-paper': openedMixin(theme),
|
||||
'& .MuiDrawer-paper': {
|
||||
...openedMixin(theme),
|
||||
backgroundColor: "unset !important",
|
||||
'& .MuiList-root': {
|
||||
backgroundColor: "unset !important",
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
props: ({ open }) => !open,
|
||||
style: {
|
||||
...closedMixin(theme),
|
||||
'& .MuiDrawer-paper': closedMixin(theme),
|
||||
'& .MuiDrawer-paper': {
|
||||
...closedMixin(theme),
|
||||
backgroundColor: "unset !important",
|
||||
'& .MuiList-root': {
|
||||
backgroundColor: "unset !important",
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -143,6 +163,7 @@ export const StyledListItem = styled(ListItem)(
|
||||
marginTop: 0,
|
||||
paddingBottom: "2px",
|
||||
color: theme.navBarTextColor,
|
||||
backgroundColor: "unset !important",
|
||||
}),
|
||||
);
|
||||
export const StyledListItemIcon = styled(ListItemIcon)(
|
||||
@@ -152,6 +173,7 @@ export const StyledListItemIcon = styled(ListItemIcon)(
|
||||
paddingBottom: 0,
|
||||
minWidth: "45px",
|
||||
//color: theme.navBarTextIconColor,
|
||||
backgroundColor: "unset !important",
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -416,7 +438,7 @@ const Eventing = () => {
|
||||
}
|
||||
const JupyterNotebook = () => {
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} target="_blank" component={Link} to='/jupyter' key={"jupyter"} >
|
||||
<StyledListItem className={classes.listSubHeader} target="_blank" component={Link} to='/jupyter' key={"jupyter"} >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Jupyter Notebooks"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<img src={JupyterLogo} height={"25px"} width={"25px"} />
|
||||
@@ -428,7 +450,7 @@ const JupyterNotebook = () => {
|
||||
}
|
||||
const GraphQL = () => {
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} target="_blank" component={Link} to='/console' key={"console"} >
|
||||
<StyledListItem className={classes.listSubHeader} target="_blank" component={Link} to='/console' key={"console"} >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"GraphQL Console"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<img src={GraphQLLogo} height={"25px"} width={"25px"} className="mythicElement"/>
|
||||
@@ -736,9 +758,10 @@ export function TopAppBarVertical(props) {
|
||||
<>
|
||||
{me?.user?.current_operation_id ? (<EventFeedNotifications me={me} />) : null }
|
||||
<Drawer anchor="left" variant="permanent" open={menuOpen} onClose={handleDrawerClose}
|
||||
style={{borderRight: "1px solid grey !important"}}>
|
||||
style={{borderRight: "1px solid grey !important", backgroundColor: "unset !important"
|
||||
}}>
|
||||
<List style={{paddingTop: 0, marginTop: 0, height: "100%", display: "flex", flexDirection: "column",
|
||||
backgroundColor: theme.topAppBarColor,
|
||||
backgroundColor: "unset !important",
|
||||
borderBottom: "unset !important", borderLeft: "unset !important", borderTop: "unset !important"}}>
|
||||
<ListItem className={classes.listSubHeader} style={{marginTop:0, paddingTop: 0, paddingLeft: "2px", paddingBottom: 0}}>
|
||||
<ListItemIcon >
|
||||
@@ -877,16 +900,29 @@ function TopBarRightShortcutsVertical({me, menuOpen, serverName}){
|
||||
{
|
||||
name: (
|
||||
<>
|
||||
<Typography paragraph={true} variant="caption" style={{marginBottom: "0"}}>Signed in as:</Typography>
|
||||
<Typography paragraph={true} variant="body1" style={{marginBottom: "0", fontWeight: 600}}> {me?.user?.username || "" } </Typography>
|
||||
</>
|
||||
<Typography variant={"caption"} style={{marginBottom: "0"}}>Signed in as:</Typography>
|
||||
<Typography variant={"body1"} style={{marginBottom: "0", fontWeight: 600}}>{me?.user?.username}</Typography>
|
||||
</>),
|
||||
disabled: true,
|
||||
click: handleSettingsClose
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div style={{display: "flex", alignItems: "center"}}>
|
||||
<TuneIcon style={{marginRight: "0.5rem"}}/>
|
||||
<Typography variant="body1" style={{marginBottom: "0", }}> Settings </Typography>
|
||||
</div>
|
||||
),
|
||||
to: "/new/settings",
|
||||
component: Link,
|
||||
click: handleSettingsClose
|
||||
},
|
||||
{
|
||||
name: "Logout",
|
||||
name: (
|
||||
<div style={{display: "flex", alignItems: "center"}}>
|
||||
<LogoutIcon style={{marginRight: "0.5rem"}} /> Logout
|
||||
</div>
|
||||
),
|
||||
click: handleLogout,
|
||||
to: "",
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {BrowserScriptsTable} from './BrowserScriptsTable';
|
||||
import {snackActions} from '../../utilities/Snackbar';
|
||||
import { Backdrop } from '@mui/material';
|
||||
import {CircularProgress} from '@mui/material';
|
||||
import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
|
||||
|
||||
|
||||
const SUB_BrowserScripts = gql`
|
||||
@@ -135,7 +136,7 @@ export function BrowserScripts({me}){
|
||||
}
|
||||
}, [])
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicPageBody>
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={false}>
|
||||
<CircularProgress color="inherit" />
|
||||
</Backdrop>
|
||||
@@ -145,6 +146,6 @@ export function BrowserScripts({me}){
|
||||
onSubmitEdit={onSubmitEdit} onRevert={onRevert} onSubmitNew={onSubmitCreateNewBrowserScript}
|
||||
/>
|
||||
|
||||
</React.Fragment>
|
||||
</MythicPageBody>
|
||||
);
|
||||
}
|
||||
@@ -13,31 +13,28 @@ import AddCircleIcon from '@mui/icons-material/AddCircle';
|
||||
import Button from '@mui/material/Button';
|
||||
import { MythicDialog } from '../../MythicComponents/MythicDialog';
|
||||
import {EditScriptDialog} from './EditScriptDialog';
|
||||
import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
|
||||
|
||||
|
||||
export function BrowserScriptsTable(props){
|
||||
const theme = useTheme();
|
||||
const [openNewScriptDialog, setOpenNewScriptDialog] = React.useState(false);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main}} variant={"elevation"}>
|
||||
<Typography variant="h5" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
|
||||
Browser Scripts
|
||||
</Typography>
|
||||
<Button size="small" onClick={() => setOpenNewScriptDialog(true)} style={{float: "right", marginRight: "30px", color: "white"}}
|
||||
<>
|
||||
<MythicPageHeader title={"Browser Scripts"}>
|
||||
<Button size="small" onClick={() => setOpenNewScriptDialog(true)} style={{whiteSpace: "nowrap", color: "white"}}
|
||||
startIcon={<AddCircleIcon color={"success"} style={{backgroundColor: "white", borderRadius: "10px"}}/>} >
|
||||
New Script
|
||||
</Button>
|
||||
{openNewScriptDialog ? (
|
||||
{openNewScriptDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openNewScriptDialog}
|
||||
onClose={()=>{setOpenNewScriptDialog(false);}}
|
||||
innerDialog={
|
||||
<EditScriptDialog me={props.me} onClose={()=>{setOpenNewScriptDialog(false);}} title="Create New Browser Script" new={true} onSubmitEdit={props.onSubmitNew} />
|
||||
} />
|
||||
) : null
|
||||
} />
|
||||
}
|
||||
</Paper>
|
||||
<TableContainer className="mythicElement" style={{maxHeight: "90%"}}>
|
||||
</MythicPageHeader>
|
||||
<TableContainer className="mythicElement" style={{height: "100%", flexGrow: 1}}>
|
||||
<Table stickyHeader={true} size="small" style={{"tableLayout": "fixed", "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
@@ -61,8 +58,8 @@ export function BrowserScriptsTable(props){
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</React.Fragment>
|
||||
</TableContainer>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import CardHeader from '@mui/material/CardHeader';
|
||||
import {gql, useQuery} from '@apollo/client';
|
||||
import { CardContent } from '@mui/material';
|
||||
import {classes, StyledButton, StyledDivider} from '../../MythicComponents/MythicTransferList';
|
||||
import {MythicDraggableDialogTitle} from "../../MythicComponents/MythicDraggableDialogTitle";
|
||||
|
||||
const getCommandsQuery = gql`
|
||||
query GetCallbackDetails($callback_id: Int!) {
|
||||
@@ -184,8 +185,8 @@ export function AddRemoveCallbackCommandsDialog(props) {
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Add or Remove Commands for Callback {props.display_id} </DialogTitle>
|
||||
<DialogContent dividers={true} style={{height: "100%", display: "flex", flexDirection: "column", position: "relative", maxHeight: "100%"}}>
|
||||
<DialogTitle>Add or Remove Commands for Callback {props.display_id} </DialogTitle>
|
||||
<DialogContent dividers={true} style={{height: "100%", display: "flex", flexDirection: "column", position: "relative", overflowY: "auto"}}>
|
||||
This will add or remove commands associated with this callback from Mythic's perspective.
|
||||
This does NOT add or remove commands within the payload itself that's beaconing out to Mythic.
|
||||
<div style={{display: "flex", flexDirection: "row", overflowY: "auto", flexGrow: 1, minHeight: 0}}>
|
||||
|
||||
@@ -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');
|
||||
@@ -257,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,7 +7,7 @@ import {
|
||||
updateSleepInfoCallbackMutation
|
||||
} from './CallbackMutations';
|
||||
import {snackActions} from '../../utilities/Snackbar';
|
||||
import {useMutation, useLazyQuery } from '@apollo/client';
|
||||
import {useMutation, useLazyQuery, gql } from '@apollo/client';
|
||||
import {
|
||||
CallbacksTableIDCell,
|
||||
CallbacksTableStringCell,
|
||||
@@ -19,8 +19,7 @@ import {
|
||||
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}
|
||||
@@ -295,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();
|
||||
}
|
||||
@@ -313,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"){
|
||||
@@ -329,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
|
||||
@@ -399,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({
|
||||
@@ -418,6 +489,7 @@ function CallbacksTablePreMemo(props){
|
||||
{
|
||||
name: 'Process Browser', icon: <AccountTreeIcon style={{paddingRight: "5px"}}/>,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const tabType = "processBrowser";
|
||||
onOpenTab({
|
||||
@@ -428,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
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -445,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);
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -549,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( () => {
|
||||
@@ -622,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) => {
|
||||
@@ -645,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);
|
||||
}
|
||||
}
|
||||
@@ -848,10 +942,10 @@ function CallbacksTablePreMemo(props){
|
||||
}
|
||||
}, [])
|
||||
}, [callbacks, sortData, filterOptions, columnVisibility, clickedCallbackID]);
|
||||
const onSubmitFilterOptions = (newFilterOptions) => {
|
||||
setFilterOptions(newFilterOptions);
|
||||
const onSubmitFilterOptions = (value) => {
|
||||
setFilterOptions({...filterOptions, [selectedColumn.key]: value });
|
||||
try{
|
||||
updateSetting({setting_name: "callbacks_table_filter_options", value: newFilterOptions});
|
||||
updateSetting({setting_name: "callbacks_table_filter_options", value: {...filterOptions, [selectedColumn.key]: value }});
|
||||
}catch(error){
|
||||
console.log("failed to save filter options");
|
||||
}
|
||||
@@ -904,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
|
||||
@@ -922,11 +1023,14 @@ function CallbacksTablePreMemo(props){
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={<TableFilterDialog
|
||||
selectedColumn={selectedColumn}
|
||||
filterOptions={filterOptions}
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
onClose={()=>{setOpenContextMenu(false);}} />}
|
||||
innerDialog={<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={filterOptions[selectedColumn.key]}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>}
|
||||
/>
|
||||
}
|
||||
{openEditDescriptionDialog &&
|
||||
@@ -1081,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>
|
||||
|
||||
@@ -249,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);}}
|
||||
|
||||
@@ -10,8 +10,12 @@ 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);
|
||||
@@ -75,94 +79,118 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clic
|
||||
<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>
|
||||
@@ -182,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}
|
||||
@@ -201,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}
|
||||
@@ -219,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
|
||||
@@ -237,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':
|
||||
@@ -255,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,7 +324,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
};
|
||||
const contextMenuOptions = [
|
||||
{
|
||||
name: 'Filter Column',
|
||||
name: 'Filter Column', type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
@@ -319,18 +339,84 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Show/Hide Columns",
|
||||
name: "Reorder Columns and Adjust Visibility",type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
setOpenAdjustColumnsDialog(true);
|
||||
setOpenReorderDialog(true);
|
||||
}
|
||||
}
|
||||
];
|
||||
const onSubmitAdjustColumns = ({left, right}) => {
|
||||
setColumnVisibility({visible: right, hidden: left});
|
||||
React.useEffect( () => {
|
||||
try {
|
||||
const storageItem = GetMythicSetting({setting_name: `file_browser_table_columns`, default_value: defaultVisibleColumns});
|
||||
if(storageItem !== null){
|
||||
let allColumns = [...columnVisibility["visible"].map(c => c), ...columnVisibility["hidden"].map(c => c)];
|
||||
let newHidden = [];
|
||||
allColumns.forEach((v,i,a) => {
|
||||
if(!storageItem.includes(v)){
|
||||
newHidden.push(v);
|
||||
}
|
||||
});
|
||||
if(storageItem.length !== 0){
|
||||
setColumnVisibility({visible: storageItem, hidden: newHidden});
|
||||
}
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_columns", error);
|
||||
}
|
||||
try {
|
||||
const storageItemOptions = GetMythicSetting({setting_name: `file_browser_filter_options`, default_value: {}});
|
||||
if(storageItemOptions !== null){
|
||||
setFilterOptions(storageItemOptions);
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_filter_options", error);
|
||||
}
|
||||
try {
|
||||
const storageColumnOrder = GetMythicSetting({setting_name: `file_browser_column_order`, default_value: columns.map(c => c.name)});
|
||||
if(storageColumnOrder !== null){
|
||||
let newOrder = [];
|
||||
for(let i = 0; i < storageColumnOrder.length; i++){
|
||||
for(let j = 0; j < columnOrder.length; j++){
|
||||
if(columnOrder[j].name === storageColumnOrder[i]){
|
||||
newOrder.push(columnOrder[j]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
setColumnOrder(newOrder);
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load file_browser_table_filter_options", error);
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
const onSubmitColumnReorder = (newOrder) => {
|
||||
let newVisible = [];
|
||||
let newHidden = [];
|
||||
for(let i = 0; i < newOrder.length; i++){
|
||||
if(newOrder[i].visible){
|
||||
newVisible.push(newOrder[i].name);
|
||||
} else {
|
||||
newHidden.push(newOrder[i].name);
|
||||
}
|
||||
}
|
||||
if(newVisible.length === 0){
|
||||
snackActions.error("Can't update to show no fields");
|
||||
return;
|
||||
}
|
||||
console.log("newOrder", newOrder)
|
||||
updateSetting({setting_name: `file_browser_column_order`, value: newOrder.map(c => c.name)});
|
||||
setColumnOrder(newOrder);
|
||||
setColumnVisibility({visible: newVisible, hidden: newHidden});
|
||||
updateSetting({setting_name: `file_browser_table_columns`, value: newVisible});
|
||||
setOpenReorderDialog(false);
|
||||
}
|
||||
const onResetColumnReorder = () => {
|
||||
onSubmitColumnReorder(columnDefaults);
|
||||
}
|
||||
const sortColumn = columns.findIndex((column) => column.key === sortData.sortKey);
|
||||
const onRowClick = ({event, rowDataStatic}) => {
|
||||
@@ -365,7 +451,9 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
copyStringToClipboard(element.name_text);
|
||||
if(copyStringToClipboard(element.name_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -373,22 +461,26 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
copyStringToClipboard(element.full_path_text);
|
||||
if(copyStringToClipboard(element.full_path_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Attributes', type: "item",
|
||||
name: 'Metadata', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
copyStringToClipboard(JSON.stringify(element?.metadata?.permissions, null, 2));
|
||||
if(copyStringToClipboard(JSON.stringify(element?.metadata?.permissions, null, 2))){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
async function optionsB (callback_id, callback_display_id, element) {
|
||||
let downloadCommand = await props.getLoadedCommandForUIFeature(callback_id, element.can_have_children ? "file_browser:download_folder" : "file_browser:download");
|
||||
let downloadCommand = await props.getLoadedCommandForUIFeature(callback_id, element.can_have_children ? props.baseUIFeature + ":download_folder" : props.baseUIFeature + ":download");
|
||||
let downloadDisplay = "Download (Unsupported)";
|
||||
if(element.can_have_children){
|
||||
downloadDisplay = "Download Folder (Unsupported)";
|
||||
@@ -399,15 +491,21 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
downloadDisplay = `Download Folder (${downloadCommand.command.cmd})`;
|
||||
}
|
||||
}
|
||||
let listCommand = await props.getLoadedCommandForUIFeature(callback_id, "file_browser:list");
|
||||
let listCommand = await props.getLoadedCommandForUIFeature(callback_id, props.baseUIFeature + ":list");
|
||||
let listDisplay = "List (Unsupported)";
|
||||
if(listCommand !== undefined){
|
||||
listDisplay = `List (${listCommand.command.cmd})`;
|
||||
}
|
||||
let removeCommand = await props.getLoadedCommandForUIFeature(callback_id, "file_browser:remove");
|
||||
let removeCommand = await props.getLoadedCommandForUIFeature(callback_id, element.can_have_children ? props.baseUIFeature + ":remove_folder" : props.baseUIFeature + ":remove");
|
||||
let removeDisplay = "Remove (Unsupported)";
|
||||
if( element.can_have_children) {
|
||||
removeDisplay = "Remove Folder (Unsupported)"
|
||||
}
|
||||
if(removeCommand !== undefined){
|
||||
removeDisplay = `Remove (${removeCommand.command.cmd})`;
|
||||
if(element.can_have_children){
|
||||
removeDisplay = `Remove Folder (${removeCommand.command.cmd})`;
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
@@ -421,7 +519,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: element.can_have_children ? 'file_browser:download_folder' : 'file_browser:download',
|
||||
uifeature: element.can_have_children ? props.baseUIFeature + ':download_folder' : props.baseUIFeature + ':download',
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
},
|
||||
@@ -436,7 +534,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: 'file_browser:list',
|
||||
uifeature: props.baseUIFeature + ':list',
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
},
|
||||
@@ -451,7 +549,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: 'file_browser:remove',
|
||||
uifeature: element.can_have_children ? props.baseUIFeature + ':remove_folder' : props.baseUIFeature + ':remove',
|
||||
getConfirmation: true,
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
@@ -476,7 +574,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
full_path: selectedRows[i].full_path_text,
|
||||
host: selectedRows[i].host,
|
||||
filename: selectedRows[i].name_text,
|
||||
uifeature: selectedRows[i].can_have_children ? 'file_browser:download_folder': 'file_browser:download',
|
||||
uifeature: selectedRows[i].can_have_children ? props.baseUIFeature + ':download_folder': props.baseUIFeature + ':download',
|
||||
callback_id: props.tabInfo.callbackID,
|
||||
callback_display_id: props.tabInfo.displayID
|
||||
});
|
||||
@@ -496,7 +594,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
full_path: selectedRows[i].full_path_text,
|
||||
host: selectedRows[i].host,
|
||||
filename: selectedRows[i].name_text,
|
||||
uifeature: 'file_browser:remove',
|
||||
uifeature: props.baseUIFeature + ':remove',
|
||||
getConfirmation: true,
|
||||
callback_id: props.tabInfo.callbackID,
|
||||
callback_display_id: props.tabInfo.displayID
|
||||
@@ -517,7 +615,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
full_path: selectedRows[i].full_path_text,
|
||||
host: selectedRows[i].host,
|
||||
filename: selectedRows[i].name_text,
|
||||
uifeature: 'file_browser:list',
|
||||
uifeature: props.baseUIFeature + ':list',
|
||||
callback_id: props.tabInfo.callbackID,
|
||||
callback_display_id: props.tabInfo.displayID
|
||||
});
|
||||
@@ -539,6 +637,21 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
}
|
||||
return options;
|
||||
}
|
||||
if(loading){
|
||||
return (
|
||||
<div style={{width: '100%', height: '100%', position: "relative",}}>
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%"
|
||||
}}>
|
||||
{"Loading Saved Browser Customizations..."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div style={{width: '100%', height: '100%', overflow: "hidden", position: "relative"}}>
|
||||
{displayFormat === "normal" &&
|
||||
@@ -619,26 +732,34 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
<RenderSingleTask task_id={props.selectedFolderData?.task_id} />
|
||||
}
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xs" open={openContextMenu}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
innerDialog={<TableFilterDialog
|
||||
selectedColumn={selectedColumn}
|
||||
filterOptions={filterOptions}
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
onClose={()=>{setOpenContextMenu(false);}} />}
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={filterOptions[selectedColumn.key]}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
{openAdjustColumnsDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openAdjustColumnsDialog}
|
||||
onClose={()=>{setOpenAdjustColumnsDialog(false);}}
|
||||
innerDialog={
|
||||
<MythicTransferListDialog onClose={()=>{setOpenAdjustColumnsDialog(false);}}
|
||||
onSubmit={onSubmitAdjustColumns} right={columnVisibility.visible} rightTitle="Show these columns"
|
||||
leftTitle={"Hidden Columns"} left={columnVisibility.hidden} dialogTitle={"Edit which columns are shown"}/>}
|
||||
{openReorderDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openReorderDialog}
|
||||
onClose={()=>{setOpenReorderDialog(false);}}
|
||||
innerDialog={
|
||||
<CallbacksTableColumnsReorderDialog
|
||||
onClose={()=>{setOpenReorderDialog(false);}}
|
||||
visible={columnVisibility.visible}
|
||||
hidden={columnVisibility.hidden}
|
||||
onReset={onResetColumnReorder}
|
||||
onSubmit={onSubmitColumnReorder}
|
||||
initialItems={columnOrder}
|
||||
/>}
|
||||
/>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -646,8 +767,8 @@ const FileBrowserTableRowNameCell = ({cellData, rowData, treeRootData, selected
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div style={{ alignItems: 'center', display: 'flex', maxHeight: "100%", textDecoration: treeRootData[selectedFolderData.host][cellData]?.deleted ? 'line-through' : '' }}>
|
||||
{!treeRootData[selectedFolderData.host][cellData]?.can_have_children ? (
|
||||
<div style={{ alignItems: 'center', display: 'flex', maxHeight: "100%", textDecoration: treeRootData[selectedFolderData.host][rowData.full_path_text]?.deleted ? 'line-through' : '' }}>
|
||||
{!treeRootData[selectedFolderData.host][rowData.full_path_text]?.can_have_children ? (
|
||||
<DescriptionIcon size={"small"} style={{
|
||||
marginRight: '5px' }} />
|
||||
) : (
|
||||
@@ -658,7 +779,7 @@ const FileBrowserTableRowNameCell = ({cellData, rowData, treeRootData, selected
|
||||
marginRight: '8px',
|
||||
marginLeft: "4px",
|
||||
color:
|
||||
treeRootData[selectedFolderData.host][cellData]?.success || treeRootData[selectedFolderData.host][cellData]?.metadata?.has_children
|
||||
treeRootData[selectedFolderData.host][rowData.full_path_text]?.success || treeRootData[selectedFolderData.host][rowData.full_path_text]?.has_children
|
||||
? theme.folderColor
|
||||
: 'grey',
|
||||
}}
|
||||
@@ -667,17 +788,17 @@ const FileBrowserTableRowNameCell = ({cellData, rowData, treeRootData, selected
|
||||
<pre
|
||||
style={{
|
||||
color:
|
||||
treeRootData[selectedFolderData.host][cellData]?.success !== null
|
||||
treeRootData[selectedFolderData.host][rowData.full_path_text]?.success !== null
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary,
|
||||
}}>
|
||||
{treeRootData[selectedFolderData.host][cellData]?.name_text}
|
||||
{cellData}
|
||||
</pre>
|
||||
{treeRootData[selectedFolderData.host][cellData]?.success === true ? (
|
||||
{treeRootData[selectedFolderData.host][rowData.full_path_text]?.success === true ? (
|
||||
<MythicStyledTooltip title='Successfully listed contents of folder' tooltipStyle={{display: "inline-flex", marginLeft: "5px"}}>
|
||||
<CheckCircleOutlineIcon color="success" fontSize='small' />
|
||||
</MythicStyledTooltip>
|
||||
) : treeRootData[selectedFolderData.host][cellData]?.success === false ? (
|
||||
) : treeRootData[selectedFolderData.host][rowData.full_path_text]?.success === false ? (
|
||||
<MythicStyledTooltip title='Failed to list contents of folder' tooltipStyle={{display: "inline-flex", marginLeft: "5px"}}>
|
||||
<ErrorIcon fontSize='small' color="error" />
|
||||
</MythicStyledTooltip>
|
||||
@@ -810,7 +931,7 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
|
||||
};
|
||||
const optionsA = [
|
||||
{
|
||||
name: treeRootData[selectedFolderData.host][cellData]?.name_text, icon: null, click: ({event}) => {},
|
||||
name: treeRootData[selectedFolderData.host][rowData.full_path_text]?.name_text, icon: null, click: ({event}) => {},
|
||||
type: "item", disabled: true
|
||||
},
|
||||
{
|
||||
@@ -818,7 +939,7 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
|
||||
icon: <VisibilityIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
setPermissionData(treeRootData[selectedFolderData.host][cellData].metadata);
|
||||
setPermissionData(treeRootData[selectedFolderData.host][rowData.full_path_text].metadata);
|
||||
setViewPermissionsDialogOpen(true);
|
||||
},
|
||||
},
|
||||
@@ -828,7 +949,7 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
getHistory({ variables: {
|
||||
full_path_text: treeRootData[selectedFolderData.host][cellData].full_path_text,
|
||||
full_path_text: treeRootData[selectedFolderData.host][rowData.full_path_text].full_path_text,
|
||||
host: selectedFolderData.host,
|
||||
group: [selectedFolderData.group],
|
||||
} });
|
||||
@@ -867,8 +988,8 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
|
||||
ref={dropdownAnchorRef}>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
{treeRootData[selectedFolderData.host][cellData]?.filemeta.length > 0 ?
|
||||
<MythicStyledTooltip title={treeRootData[selectedFolderData.host][cellData]?.filemeta[0]?.complete ?
|
||||
{treeRootData[selectedFolderData.host][rowData.full_path_text]?.filemeta.length > 0 ?
|
||||
<MythicStyledTooltip title={treeRootData[selectedFolderData.host][rowData.full_path_text]?.filemeta[0]?.complete ?
|
||||
"Preview Media" : "Preview Partial Media"}>
|
||||
<FontAwesomeIcon icon={faPhotoVideo} size={"1x"} style={{
|
||||
position: "relative", cursor: "pointer", display: "inline-block"}}
|
||||
@@ -880,8 +1001,8 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openPreviewMediaDialog}
|
||||
onClose={(e)=>{setOpenPreviewMediaDialog(false);}}
|
||||
innerDialog={<PreviewFileMediaDialog
|
||||
agent_file_id={treeRootData[selectedFolderData.host][cellData]?.filemeta[0]?.agent_file_id}
|
||||
filename={b64DecodeUnicode(treeRootData[selectedFolderData.host][cellData]?.filemeta[0]?.filename_text)}
|
||||
agent_file_id={treeRootData[selectedFolderData.host][rowData.full_path_text]?.filemeta[0]?.agent_file_id}
|
||||
filename={b64DecodeUnicode(treeRootData[selectedFolderData.host][rowData.full_path_text]?.filemeta[0]?.filename_text)}
|
||||
onClose={(e)=>{setOpenPreviewMediaDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -6,11 +6,14 @@ import ListIcon from '@mui/icons-material/List';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import {getAllParentNodes} from "./CallbacksTabsFileBrowser";
|
||||
import {Dropdown, DropdownMenuItem, DropdownNestedMenuItem} from "../../MythicComponents/MythicNestedMenus";
|
||||
import {copyStringToClipboard} from "../../utilities/Clipboard";
|
||||
import FileCopyOutlinedIcon from '@mui/icons-material/FileCopyOutlined';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
|
||||
export const getOpenIDFromNode = (node) => {
|
||||
return `${node.group};${node.host};${node.full_path_text}`;
|
||||
}
|
||||
export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatrix, fetchFolderData, setTableData, taskListing, tableOpenedPathId, showDeletedFiles, tabInfo, selectedFolderData}) => {
|
||||
export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatrix, fetchFolderData, setTableData, taskListing, tableOpenedPathId, showDeletedFiles, tabInfo, selectedFolderData, baseUIFeature}) => {
|
||||
const [openNodes, setOpenNodes] = React.useState({});
|
||||
const groupName = React.useRef("");
|
||||
const [openViewGroupsDialog, setOpenViewGroupDialog] = React.useState(false);
|
||||
@@ -19,7 +22,7 @@ export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatri
|
||||
//console.log("toggleNodeExpanded", nodeId, nodeData);
|
||||
let lastOpenedNodeNewState = true;
|
||||
let lastOpenedNodeID = getOpenIDFromNode(lastOpenedNodeRef.current);
|
||||
if(!lastOpenedNodeRef.current?.metadata?.has_children){
|
||||
if(!lastOpenedNodeRef.current?.has_children){
|
||||
lastOpenedNodeNewState = false;
|
||||
}
|
||||
lastOpenedNodeRef.current = {...nodeData};
|
||||
@@ -28,7 +31,7 @@ export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatri
|
||||
setOpenNodes({
|
||||
...openNodes,
|
||||
[getOpenIDFromNode(nodeData)]: true,
|
||||
[lastOpenedNodeID]: lastOpenedNodeNewState
|
||||
//[lastOpenedNodeID]: lastOpenedNodeNewState
|
||||
});
|
||||
};
|
||||
const toggleNodeCollapsed = (nodeId, nodeData) => {
|
||||
@@ -77,6 +80,41 @@ export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatri
|
||||
});
|
||||
}, [selectedFolderData]);
|
||||
const contextMenuOptions= (callback_id, callback_display_id, node) => [
|
||||
{
|
||||
name: "Copy to Clipboard", icon: null, click: () => {}, type: "menu",
|
||||
menuItems: [
|
||||
{
|
||||
name: 'Name', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(node.name_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Full Path', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(node.full_path_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Metadata', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(JSON.stringify(node?.metadata, null, 2))){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'List', type: "item", icon: <ListIcon color="warning" style={{ paddingRight: '5px'}} />,
|
||||
click: ({event}) => {
|
||||
@@ -146,6 +184,7 @@ export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatri
|
||||
onCollapseNode={toggleNodeCollapsed}
|
||||
onContextMenu={onContextMenu}
|
||||
tabInfo={tabInfo}
|
||||
baseUIFeature={baseUIFeature}
|
||||
/>
|
||||
{openViewGroupsDialog &&
|
||||
<MythicDialog
|
||||
|
||||
@@ -8,6 +8,7 @@ import {hideCallbacksMutation} from './CallbackMutations';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import {CallbacksTableLastCheckinCell, CallbacksTablePayloadTypeCell, CallbacksTableIPCell} from "./CallbacksTableRow";
|
||||
import { DataGrid } from '@mui/x-data-grid';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
|
||||
|
||||
const callbacksAndFeaturesQuery = gql`
|
||||
@@ -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", "Arch", "Session", "User", "CMD"],
|
||||
"visible": defaultVisibleColumns,
|
||||
"hidden": [ "Comment", "Tags" ]
|
||||
})
|
||||
const [singleTreeData, setSingleTreeData] = React.useState({});
|
||||
const [viewSingleTreeData, setViewSingleTreeData] = React.useState(false);
|
||||
const [openAdjustColumnsDialog, setOpenAdjustColumnsDialog] = React.useState(false);
|
||||
const [openReorderDialog, setOpenReorderDialog] = React.useState(false);
|
||||
const [columnOrder, setColumnOrder] = React.useState(columnDefaults);
|
||||
const [updatedTreeAdjMatrix, setUpdatedTreeAdjMatrix] = React.useState(treeAdjMatrix);
|
||||
const openAllNodes = (state) => {
|
||||
let onodes = {};
|
||||
@@ -192,21 +211,10 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
// }
|
||||
|
||||
};
|
||||
const columnDefaults = [
|
||||
{ name: 'Info', width: 50, disableAutosize: true, disableSort: true, disableFilterMenu: true },
|
||||
{ name: 'PID', type: 'number', key: 'process_id', inMetadata: true, width: 100},
|
||||
{ name: 'PPID', type: 'number', key: 'parent_process_id', inMetadata: true, width: 100},
|
||||
{ name: 'Name', type: 'string', disableSort: false, key: 'name_text', fillWidth: true },
|
||||
{ name: "Arch", type: 'string', key: 'architecture', inMetadata: true, width: 70},
|
||||
{ name: 'Session', type: 'number', key: 'session_id', inMetadata: true, width: 100},
|
||||
{ name: "User", type: 'string', key: 'user', inMetadata: true, fillWidth: true},
|
||||
{ name: 'Tags', type: 'tags', disableSort: true, disableFilterMenu: true, width: 220 },
|
||||
{ name: 'Comment', type: 'string', key: 'comment', disableSort: false, width: 200 },
|
||||
{ name: "CMD", type: "string", key: 'command_line', inMetadata: true, fillWidth: true},
|
||||
];
|
||||
|
||||
const columns = React.useMemo(
|
||||
() =>
|
||||
columnDefaults.reduce( (prev, cur) => {
|
||||
() =>
|
||||
columnOrder.reduce( (prev, cur) => {
|
||||
if(columnVisibility.visible.includes(cur.name)){
|
||||
if(filterOptions[cur.key] && String(filterOptions[cur.key]).length > 0){
|
||||
return [...prev, {...cur, filtered: true}];
|
||||
@@ -217,7 +225,7 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
return [...prev];
|
||||
}
|
||||
}, [])
|
||||
, [filterOptions, columnVisibility]
|
||||
, [filterOptions, columnVisibility, columnOrder]
|
||||
);
|
||||
const flattenNode = useCallback(
|
||||
(node, host, group, depth = 0) => {
|
||||
@@ -384,8 +392,14 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
}
|
||||
return tempData;
|
||||
}, [allData, sortData]);
|
||||
const onSubmitFilterOptions = (newFilterOptions) => {
|
||||
setFilterOptions(newFilterOptions);
|
||||
const onSubmitFilterOptions = (value) => {
|
||||
let newValue = value ? value : "";
|
||||
try{
|
||||
setFilterOptions({...filterOptions, [selectedColumn.current.key]: newValue });
|
||||
updateSetting({setting_name: `process_browser_filter_options`, value: {...filterOptions,[selectedColumn.current.key]: newValue }});
|
||||
}catch(error){
|
||||
console.log("failed to save filter options", error, filterOptions, selectedColumn.current.key, newValue);
|
||||
}
|
||||
if(viewSingleTreeData){
|
||||
return
|
||||
}
|
||||
@@ -482,7 +496,7 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
}
|
||||
|
||||
setSingleTreeData(singleTreeAdjMatrix);
|
||||
onSubmitFilterOptions({});
|
||||
onSubmitFilterOptions(undefined);
|
||||
}
|
||||
const toggleViewSingleTreeData = () => {
|
||||
setViewSingleTreeData(!viewSingleTreeData);
|
||||
@@ -550,6 +564,13 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
group={group}
|
||||
rowData={row}
|
||||
cellData={treeRootData[group][host][row.full_path_text]?.parent_path_text || ""} />;
|
||||
case "Integrity":
|
||||
return <FileBrowserTableRowStringCell
|
||||
treeRootData={treeRootData[group]}
|
||||
host={host}
|
||||
group={group}
|
||||
rowData={row}
|
||||
cellData={treeRootData[group][host][row.full_path_text ]?.metadata?.integrity_level || ''} />;
|
||||
case "Tags":
|
||||
return <FileBrowserTagsCell
|
||||
rowData={row}
|
||||
@@ -574,7 +595,7 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
cellData={treeRootData[group][host][row.full_path_text ]?.metadata?.command_line || ""}
|
||||
/>;
|
||||
default:
|
||||
console.log("hit default case in switch on c.name)")
|
||||
console.log("hit default case in switch on c.name)", c.name)
|
||||
}
|
||||
})];
|
||||
}
|
||||
@@ -605,7 +626,7 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
};
|
||||
const contextMenuOptions = [
|
||||
{
|
||||
name: 'Filter Column',
|
||||
name: 'Filter Column', type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
@@ -620,7 +641,7 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Show/Hide Columns",
|
||||
name: "Reorder Columns and Adjust Visibility", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
@@ -630,12 +651,77 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
snackActions.warning("Can't filter that column");
|
||||
return;
|
||||
}
|
||||
setOpenAdjustColumnsDialog(true);
|
||||
setOpenReorderDialog(true);
|
||||
}
|
||||
}
|
||||
];
|
||||
const onSubmitAdjustColumns = ({left, right}) => {
|
||||
setColumnVisibility({visible: right, hidden: left});
|
||||
React.useEffect( () => {
|
||||
try {
|
||||
const storageItem = GetMythicSetting({setting_name: `process_browser_table_columns`, default_value: defaultVisibleColumns});
|
||||
if(storageItem !== null){
|
||||
let allColumns = [...columnVisibility["visible"].map(c => c), ...columnVisibility["hidden"].map(c => c)];
|
||||
let newHidden = [];
|
||||
allColumns.forEach((v,i,a) => {
|
||||
if(!storageItem.includes(v)){
|
||||
newHidden.push(v);
|
||||
}
|
||||
});
|
||||
if(storageItem.length !== 0){
|
||||
setColumnVisibility({visible: storageItem, hidden: newHidden});
|
||||
}
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_columns", error);
|
||||
}
|
||||
try {
|
||||
const storageItemOptions = GetMythicSetting({setting_name: `process_browser_filter_options`, default_value: {}});
|
||||
if(storageItemOptions !== null){
|
||||
setFilterOptions(storageItemOptions);
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_filter_options", error);
|
||||
}
|
||||
try {
|
||||
const storageColumnOrder = GetMythicSetting({setting_name: `process_browser_column_order`, default_value: columns.map(c => c.name)});
|
||||
if(storageColumnOrder !== null){
|
||||
let newOrder = [];
|
||||
for(let i = 0; i < storageColumnOrder.length; i++){
|
||||
for(let j = 0; j < columnOrder.length; j++){
|
||||
if(columnOrder[j].name === storageColumnOrder[i]){
|
||||
newOrder.push(columnOrder[j]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
setColumnOrder(newOrder);
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load process_browser_table_filter_options", error);
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
const onSubmitColumnReorder = (newOrder) => {
|
||||
let newVisible = [];
|
||||
let newHidden = [];
|
||||
for(let i = 0; i < newOrder.length; i++){
|
||||
if(newOrder[i].visible){
|
||||
newVisible.push(newOrder[i].name);
|
||||
} else {
|
||||
newHidden.push(newOrder[i].name);
|
||||
}
|
||||
}
|
||||
if(newVisible.length === 0){
|
||||
snackActions.error("Can't update to show no fields");
|
||||
return;
|
||||
}
|
||||
updateSetting({setting_name: `process_browser_column_order`, value: newOrder.map(c => c.name)});
|
||||
setColumnOrder(newOrder);
|
||||
setColumnVisibility({visible: newVisible, hidden: newHidden});
|
||||
updateSetting({setting_name: `process_browser_table_columns`, value: newVisible});
|
||||
setOpenReorderDialog(false);
|
||||
}
|
||||
const onResetColumnReorder = () => {
|
||||
onSubmitColumnReorder(columnDefaults);
|
||||
}
|
||||
const sortColumn = columns.findIndex((column) => column.key === sortData.sortKey);
|
||||
React.useEffect( () => {
|
||||
@@ -648,6 +734,21 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
openAllNodes(false);
|
||||
}
|
||||
}, [expandOrCollapseAll, updatedTreeAdjMatrix]);
|
||||
if(loading){
|
||||
return (
|
||||
<div style={{width: '100%', height: '100%', position: "relative",}}>
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%"
|
||||
}}>
|
||||
{"Loading Saved Browser Customizations..."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%', overflow: "hidden", position: "relative" }}>
|
||||
<MythicResizableGrid
|
||||
@@ -661,24 +762,34 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
/>
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xs" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={<TableFilterDialog
|
||||
selectedColumn={selectedColumn.current}
|
||||
filterOptions={filterOptions}
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
onClose={()=>{setOpenContextMenu(false);}} />}
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={String(filterOptions[selectedColumn.current.key])}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
{openAdjustColumnsDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openAdjustColumnsDialog}
|
||||
onClose={()=>{setOpenAdjustColumnsDialog(false);}}
|
||||
innerDialog={
|
||||
<MythicTransferListDialog onClose={()=>{setOpenAdjustColumnsDialog(false);}}
|
||||
onSubmit={onSubmitAdjustColumns} right={columnVisibility.visible} rightTitle="Show these columns"
|
||||
leftTitle={"Hidden Columns"} left={columnVisibility.hidden} dialogTitle={"Edit which columns are shown"}/>}
|
||||
{openReorderDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openReorderDialog}
|
||||
onClose={()=>{setOpenReorderDialog(false);}}
|
||||
innerDialog={
|
||||
<CallbacksTableColumnsReorderDialog
|
||||
onClose={()=>{setOpenReorderDialog(false);}}
|
||||
visible={columnVisibility.visible}
|
||||
hidden={columnVisibility.hidden}
|
||||
onReset={onResetColumnReorder}
|
||||
onSubmit={onSubmitColumnReorder}
|
||||
initialItems={columnOrder}
|
||||
/>}
|
||||
/>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {CallbacksTableIPCell, CallbacksTableLastCheckinCell} from "./CallbacksTa
|
||||
import { DataGrid } from '@mui/x-data-grid';
|
||||
import { validate as uuidValidate } from 'uuid';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
|
||||
|
||||
const callbacksAndFeaturesQuery = gql`
|
||||
@@ -291,12 +292,16 @@ export function CallbacksTabsTaskMultipleDialog({onClose, callback}) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Task Multiple {callback.payload.payloadtype.name} Callbacks at Once</DialogTitle>
|
||||
<DialogContentText style={{textAlign: "center"}}>
|
||||
<b>Note: </b> Last checkin times are based on when this window opened and won't refresh.
|
||||
</DialogContentText>
|
||||
<CustomSelectTable initialData={initialData}
|
||||
selectedData={selectedData} />
|
||||
<Grid size={12}>
|
||||
<CallbacksTabsTaskingInput filterTasks={false} onSubmitFilter={()=>{}} onSubmitCommandLine={onSubmitCommandLine}
|
||||
changeSelectedToken={changeSelectedToken}
|
||||
payloadtype_name={callback.payload.payloadtype.name}
|
||||
hide_context={true}
|
||||
filterOptions={{}} callback_id={callback.id} callback_os={callback.payload.os} parentMountedRef={mountedRef} />
|
||||
</Grid>
|
||||
{openTaskingButton &&
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -264,6 +264,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
const me = useReactiveVar(meState);
|
||||
const useDisplayParamsForCLIHistoryUserSetting = React.useRef(GetMythicSetting({setting_name: "useDisplayParamsForCLIHistory", default_value: operatorSettingDefaults.useDisplayParamsForCLIHistory}));
|
||||
const hideTaskingContext = React.useRef(GetMythicSetting({setting_name: "hideTaskingContext", default_value: operatorSettingDefaults.hideTaskingContext}));
|
||||
hideTaskingContext.current = props.hide_context | hideTaskingContext.current;
|
||||
const taskingContextFields = React.useRef(GetMythicSetting({setting_name: "taskingContextFields", default_value: operatorSettingDefaults.taskingContextFields}));
|
||||
const forwardOrBackwardTabIndex = (event, currentIndex, options) => {
|
||||
if(event.shiftKey){
|
||||
@@ -284,6 +285,9 @@ export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
return;
|
||||
}
|
||||
tokenOptions.current = data.data.callbacktoken;
|
||||
if(tokenOptions.current.length === 0) {
|
||||
props.changeSelectedToken("Default Token");
|
||||
}
|
||||
}
|
||||
});
|
||||
useSubscription(contextSubscription, {
|
||||
@@ -789,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;
|
||||
}
|
||||
|
||||
@@ -823,7 +827,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
buffer += value;
|
||||
return;
|
||||
}
|
||||
else if(value === " "){
|
||||
else if([" ", "\t", "\r", "\n"].includes(value)){
|
||||
if(backslash){
|
||||
backslash = false;
|
||||
//buffer += " ";
|
||||
@@ -1794,11 +1798,11 @@ export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
innerDialog={<MythicSelectFromListDialog onClose={() => {
|
||||
setOpenSelectCommandDialog(false);
|
||||
}}
|
||||
onSubmit={processCommandAndCommandLine}
|
||||
options={commandOptions.current}
|
||||
title={"Select Command"}
|
||||
action={"select"} identifier={"id"}
|
||||
display={"display"}/>}
|
||||
onSubmit={processCommandAndCommandLine}
|
||||
options={commandOptions.current}
|
||||
title={"Select Command"}
|
||||
action={"select"} identifier={"id"}
|
||||
display={"display"}/>}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ export function CallbacksTabsTaskingInputTokenSelect(props) {
|
||||
}
|
||||
}
|
||||
return (
|
||||
<FormControl style={{width: "20%"}}>
|
||||
<FormControl style={{width: props.width ? props.width : "20%"}}>
|
||||
<Select
|
||||
labelId="demo-dialog-select-label"
|
||||
id="demo-dialog-select"
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -168,6 +168,8 @@ callback(where: {active: {_eq: true}}) {
|
||||
callbackc2profiles {
|
||||
c2profile {
|
||||
name
|
||||
has_logo
|
||||
is_p2p
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,6 +203,8 @@ callback(where: {active: {_eq: true}}) {
|
||||
callbackc2profiles {
|
||||
c2profile {
|
||||
name
|
||||
has_logo
|
||||
is_p2p
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,6 +212,7 @@ callback(where: {active: {_eq: true}}) {
|
||||
id
|
||||
is_p2p
|
||||
name
|
||||
has_logo
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -248,6 +253,8 @@ callbackgraphedge_stream(batch_size: 100, cursor: {initial_value: {updated_at: $
|
||||
callbackc2profiles {
|
||||
c2profile {
|
||||
name
|
||||
has_logo
|
||||
is_p2p
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -282,6 +289,8 @@ callbackgraphedge_stream(batch_size: 100, cursor: {initial_value: {updated_at: $
|
||||
callbackc2profiles {
|
||||
c2profile {
|
||||
name
|
||||
has_logo
|
||||
is_p2p
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -289,6 +298,7 @@ callbackgraphedge_stream(batch_size: 100, cursor: {initial_value: {updated_at: $
|
||||
id
|
||||
is_p2p
|
||||
name
|
||||
has_logo
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,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,
|
||||
@@ -386,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);
|
||||
}
|
||||
}
|
||||
@@ -408,7 +420,9 @@ export function CallbacksTop(props){
|
||||
callbackDescription: callbacks.current[i]["description"],
|
||||
host: callbacks.current[i]["host"],
|
||||
color: callbacks.current[i]["color"],
|
||||
os: callbacks.current[i]["payload"]["os"]};
|
||||
os: callbacks.current[i]["payload"]["os"],
|
||||
customBrowser: tabs[j].customBrowser,
|
||||
};
|
||||
newTabData.push(tabData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ export function DownloadHistoryDialog(props){
|
||||
<TableRow>
|
||||
<TableCell style={{}}></TableCell>
|
||||
<TableCell style={{width: "15rem"}}>Time</TableCell>
|
||||
<TableCell style={{width: "5rem"}}>Task</TableCell>
|
||||
<TableCell style={{width: "5rem"}}>Callback</TableCell>
|
||||
<TableCell style={{width: "7rem"}}>Task</TableCell>
|
||||
<TableCell style={{width: "7rem"}}>Callback</TableCell>
|
||||
<TableCell>Comment</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
@@ -16,6 +16,8 @@ import {ResponseDisplayInteractive} from "./ResponseDisplayInteractive";
|
||||
import {ResponseDisplayMedia} from "./ResponseDisplayMedia";
|
||||
import {GetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {ResponseDisplayGraph} from "./ResponseDisplayGraph";
|
||||
import {operatorSettingDefaults} from "../../../cache";
|
||||
import {ResponseDisplayTabs} from "./ResponseDisplayTabs";
|
||||
|
||||
const subResponsesStream = gql`
|
||||
subscription subResponsesStream($task_id: Int!){
|
||||
@@ -95,13 +97,14 @@ export const ResponseDisplay = (props) =>{
|
||||
const interactive = props?.task?.command?.supported_ui_features.includes("task_response:interactive") || false;
|
||||
return (
|
||||
interactive ? (
|
||||
<ResponseDisplayInteractive {...props} />
|
||||
<ResponseDisplayInteractive {...props} key={props?.task?.id} />
|
||||
) : (
|
||||
<NonInteractiveResponseDisplay {...props} />
|
||||
<NonInteractiveResponseDisplay {...props} key={props?.task?.id} />
|
||||
)
|
||||
)
|
||||
}
|
||||
const NonInteractiveResponseDisplay = (props) => {
|
||||
const theme = useTheme();
|
||||
const [output, setOutput] = React.useState("");
|
||||
const [rawResponses, setRawResponses] = React.useState([]);
|
||||
const seenResponseIDs = React.useRef([]);
|
||||
@@ -109,7 +112,7 @@ const NonInteractiveResponseDisplay = (props) => {
|
||||
const [totalCount, setTotalCount] = React.useState(0);
|
||||
const [openBackdrop, setOpenBackdrop] = React.useState(true);
|
||||
const togglingAllOutputToPaginated = React.useRef(false);
|
||||
const initialResponseStreamLimit = GetMythicSetting({setting_name: "experiment-responseStreamLimit", default_value: 50});
|
||||
const initialResponseStreamLimit = GetMythicSetting({setting_name: "experiment-responseStreamLimit", default_value: operatorSettingDefaults["experiment-responseStreamLimit"]});
|
||||
const [fetchMoreResponses] = useLazyQuery(getResponsesLazyQuery, {
|
||||
fetchPolicy: "network-only",
|
||||
onCompleted: (data) => {
|
||||
@@ -289,16 +292,19 @@ const NonInteractiveResponseDisplay = (props) => {
|
||||
}, [search.current]);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<Backdrop open={openBackdrop} onClick={()=>{setOpenBackdrop(false);}} style={{zIndex: 2, position: "absolute"}}>
|
||||
<CircularProgress color="inherit" disableShrink />
|
||||
</Backdrop>
|
||||
{props.searchOutput &&
|
||||
<SearchBar onSubmitSearch={onSubmitSearch} />
|
||||
}
|
||||
{!openBackdrop &&
|
||||
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%"}}>
|
||||
|
||||
{!openBackdrop &&
|
||||
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%",
|
||||
backgroundColor: theme.outputBackgroundColor + (theme.palette.mode === 'dark' ? "D0" : "D0"),
|
||||
color: theme.outputTextColor,
|
||||
}}>
|
||||
{props.searchOutput &&
|
||||
<SearchBar onSubmitSearch={onSubmitSearch} />
|
||||
}
|
||||
<div style={{overflowY: "auto", flexGrow: 1, width: "100%", height: props.expand ? "100%": undefined, display: "flex", flexDirection: "column"}} ref={props.responseRef}>
|
||||
<ResponseDisplayComponent rawResponses={rawResponses} viewBrowserScript={props.viewBrowserScript}
|
||||
output={output} command_id={props.command_id} displayType={"accordion"}
|
||||
@@ -312,7 +318,7 @@ const NonInteractiveResponseDisplay = (props) => {
|
||||
</div>
|
||||
|
||||
}
|
||||
</React.Fragment>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export const ResponseDisplayConsole = (props) => {
|
||||
@@ -402,7 +408,7 @@ export const PaginationBar = ({selectAllOutput, totalCount, onSubmitPageChange,
|
||||
}, [totalCount, maxCount, search, selectAllOutput]);
|
||||
const pageCount = Math.max(1, Math.ceil(localTotalCount / pageSize));
|
||||
// don't bother people with pagination information if they haven't even started paginating
|
||||
if(pageCount < 2){
|
||||
if(pageCount < 2 || pageCount === Infinity || isNaN(pageCount)){
|
||||
return (<div id={'scrolltotaskbottom' + task.id}></div>)
|
||||
}
|
||||
return (
|
||||
@@ -540,47 +546,64 @@ const ResponseDisplayComponent = ({rawResponses, viewBrowserScript, output, comm
|
||||
return null
|
||||
}
|
||||
return (
|
||||
localViewBrowserScript && Object.keys(browserScriptData).length > 0 ? (
|
||||
<React.Fragment>
|
||||
{browserScriptData?.screenshot?.map( (scr, index) => (
|
||||
<ResponseDisplayScreenshot key={"screenshot" + index + 'fortask' + task.id} task={task} {...scr}
|
||||
displayType={displayType} expand={expand} />
|
||||
))
|
||||
}
|
||||
{browserScriptData?.plaintext !== undefined &&
|
||||
<ResponseDisplayPlaintext plaintext={browserScriptData["plaintext"]} task={task}
|
||||
expand={expand} displayType={displayType} />
|
||||
}
|
||||
{browserScriptData?.table?.map( (table, index) => (
|
||||
|
||||
<ResponseDisplayTable callback_id={task.callback_id} task={task} expand={expand}
|
||||
table={table} key={"tablefortask" + task.id + "table" + index}
|
||||
displayType={displayType}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{browserScriptData?.download?.map( (dl, index) => (
|
||||
<ResponseDisplayDownload download={dl} task={task} displayType={displayType}
|
||||
key={"download" + index + "fortask" + task.id} />
|
||||
))
|
||||
}
|
||||
{browserScriptData?.search?.map( (s, index) => (
|
||||
<ResponseDisplaySearch search={s} task={task} displayType={displayType}
|
||||
key={"searchlink" + index + "fortask" + task.id} />
|
||||
))
|
||||
}
|
||||
{browserScriptData?.media?.map( (s, index) => (
|
||||
<ResponseDisplayMedia key={"searchmedia" + index + "fortask" + task.id}
|
||||
displayType={displayType}
|
||||
task={task} media={s} expand={expand} />
|
||||
))}
|
||||
{browserScriptData?.graph !== undefined &&
|
||||
<ResponseDisplayGraph graph={browserScriptData.graph} task={task}
|
||||
expand={expand} displayType={displayType} />
|
||||
}
|
||||
</React.Fragment>
|
||||
localViewBrowserScript ? (
|
||||
<ResponseDisplayBrowserScriptComponent expand={expand} displayType={displayType}
|
||||
task={task} browserScriptData={browserScriptData}
|
||||
output={output}/>
|
||||
) : (
|
||||
<ResponseDisplayPlaintext plaintext={output} task={task} expand={expand} displayType={displayType}/>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export function ResponseDisplayBrowserScriptComponent({output, browserScriptData, task, expand, displayType}) {
|
||||
return (
|
||||
<>
|
||||
{Object.keys(browserScriptData).length > 0 ? (
|
||||
<>
|
||||
{browserScriptData?.screenshot?.map( (scr, index) => (
|
||||
<ResponseDisplayScreenshot key={"screenshot" + index + 'fortask' + task.id} task={task} {...scr}
|
||||
displayType={displayType} expand={expand} />
|
||||
))
|
||||
}
|
||||
{browserScriptData?.plaintext !== undefined &&
|
||||
<ResponseDisplayPlaintext plaintext={browserScriptData["plaintext"]} task={task}
|
||||
expand={expand} displayType={displayType} />
|
||||
}
|
||||
{browserScriptData?.table?.map( (table, index) => (
|
||||
<ResponseDisplayTable callback_id={task.callback_id} task={task} expand={expand}
|
||||
table={table} key={"tablefortask" + task.id + "table" + index}
|
||||
displayType={displayType}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{browserScriptData?.download?.map( (dl, index) => (
|
||||
<ResponseDisplayDownload download={dl} task={task} displayType={displayType}
|
||||
key={"download" + index + "fortask" + task.id} />
|
||||
))
|
||||
}
|
||||
{browserScriptData?.search?.map( (s, index) => (
|
||||
<ResponseDisplaySearch search={s} task={task} displayType={displayType}
|
||||
key={"searchlink" + index + "fortask" + task.id} />
|
||||
))
|
||||
}
|
||||
{browserScriptData?.media?.map( (s, index) => (
|
||||
<ResponseDisplayMedia key={"searchmedia" + index + "fortask" + task.id}
|
||||
displayType={displayType}
|
||||
task={task} media={s} expand={expand} />
|
||||
))}
|
||||
{browserScriptData?.graph !== undefined &&
|
||||
<ResponseDisplayGraph graph={browserScriptData.graph} task={task}
|
||||
expand={expand} displayType={displayType} />
|
||||
}
|
||||
{browserScriptData.tabs && browserScriptData.tabs.length > 0 &&
|
||||
<ResponseDisplayTabs task={task} expand={expand} displayType={displayType} tabs={browserScriptData.tabs} output={output} />
|
||||
}
|
||||
</>
|
||||
) : (
|
||||
<ResponseDisplayPlaintext plaintext={output} task={task} expand={expand} displayType={displayType}/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import WrapTextIcon from '@mui/icons-material/WrapText';
|
||||
import { useReactiveVar } from '@apollo/client';
|
||||
import { meState } from '../../../cache';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import HeightIcon from '@mui/icons-material/Height';
|
||||
|
||||
const getInteractiveTaskingQuery = gql`
|
||||
${taskingDataFragment}
|
||||
@@ -168,7 +169,6 @@ export const GetOutputFormatAll = ({data, myTask, taskID, useASNIColor, message
|
||||
|
||||
}
|
||||
|
||||
|
||||
const InteractiveMessageTypes = [
|
||||
{"name": "None", "value": -1, "text": "None"},
|
||||
{"name": "Tab", "value": 13, "text": "^I"},
|
||||
@@ -202,8 +202,9 @@ const EnterOptions = [
|
||||
];
|
||||
export const ResponseDisplayInteractive = (props) =>{
|
||||
const me = useReactiveVar(meState);
|
||||
const theme = useTheme();
|
||||
const [backdropOpen, setBackdropOpen] = React.useState(false);
|
||||
const [scrollToBottom, setScrollToBottom] = React.useState(false);
|
||||
const [scrollToBottom, setScrollToBottom] = React.useState(true);
|
||||
const pageSize = React.useRef(100);
|
||||
const highestFetched = React.useRef(0);
|
||||
const [taskData, setTaskData] = React.useState([]);
|
||||
@@ -218,6 +219,7 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
const [useASNIColor, setUseANSIColor] = React.useState(true);
|
||||
const [showTaskStatus, setShowTaskStatus] = React.useState(true);
|
||||
const [wrapText, setWrapText] = React.useState(true);
|
||||
const [autoScroll, setAutoScroll] = React.useState(true);
|
||||
const {loading: loadingTasks} = useSubscription(getInteractiveTaskingQuery, {
|
||||
variables: {parent_task_id: props.task.id},
|
||||
onError: data => {
|
||||
@@ -279,8 +281,7 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
if(backdropOpen){
|
||||
setBackdropOpen(false);
|
||||
}
|
||||
|
||||
}, [highestFetched.current, rawResponses, props.task.id, backdropOpen, taskIDRef.current]);
|
||||
}, [highestFetched.current, rawResponses, props.task.id, backdropOpen, taskIDRef.current, autoScroll]);
|
||||
useSubscription(subResponsesQuery, {
|
||||
variables: {task_id: props.task.id},
|
||||
fetchPolicy: "no-cache",
|
||||
@@ -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,6 +41,7 @@ import 'ace-builds/src-noconflict/mode-apache_conf';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import TabContext from '@mui/lab/TabContext';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import {previewFileQuery} from "../Search/FileMetaTable";
|
||||
import { useMutation, gql, useQuery } from '@apollo/client';
|
||||
@@ -48,12 +49,20 @@ import CodeIcon from '@mui/icons-material/Code';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import {b64DecodeUnicode} from "./ResponseDisplay";
|
||||
import {MythicDialog, TableRowSizeCell} from "../../MythicComponents/MythicDialog";
|
||||
import {Table, TableHead, TableRow, TableBody} from '@mui/material';
|
||||
import {Table, TableHead, TableRow, TableBody, TableCell, Paper} from '@mui/material';
|
||||
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
|
||||
import WarningOutlinedIcon from '@mui/icons-material/WarningOutlined';
|
||||
import {TagsDisplay, ViewEditTags} from "../../MythicComponents/MythicTag";
|
||||
import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery";
|
||||
import {ResponseDisplayScreenshotModal} from "./ResponseDisplayScreenshotModal";
|
||||
import initSQLJS from 'sql.js';
|
||||
import TabPanel from '@mui/lab/TabPanel';
|
||||
import TabList from '@mui/lab/TabList';
|
||||
import {Backdrop, CircularProgress} from '@mui/material';
|
||||
// Required to let webpack 4 know it needs to copy the wasm file to our assets
|
||||
// eslint-disable-next-line import/no-webpack-loader-syntax
|
||||
import sqlWasm from "!!file-loader?name=sql-wasm-[contenthash].wasm!sql.js/dist/sql-wasm.wasm";
|
||||
import {copyStringToClipboard} from "../../utilities/Clipboard";
|
||||
|
||||
export const modeOptions = ["csharp", "golang", "html", "json", "markdown", "ruby", "python", "java",
|
||||
"javascript", "yaml", "toml", "swift", "sql", "rust", "powershell", "pgsql", "perl", "php", "objectivec",
|
||||
@@ -121,7 +130,6 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
<div style={{display: "flex", height: "100%", flexDirection: "column"}}>
|
||||
<DisplayFileMetaData fileMetaData={fileMetaData} />
|
||||
<AppBar color={'default'} position='static' className={"no-box-shadow"}>
|
||||
|
||||
<Tabs
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
@@ -139,6 +147,7 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
<Tab className={value === 0 ? "selectedCallback": ""} label={"Preview"}></Tab>
|
||||
<Tab className={value === 1 ? "selectedCallback": ""} label={"Text"}></Tab>
|
||||
<Tab className={value === 2 ? "selectedCallback": ""} label={"Hex"}></Tab>
|
||||
<Tab className={value === 3 ? "selectedCallback": ""} label={"Database"}></Tab>
|
||||
<MythicStyledTooltip title={"Download the file"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<Button style={{}} size={"small"} href={"/direct/download/" + media.agent_file_id}
|
||||
download color={"success"}>
|
||||
@@ -147,7 +156,7 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
</MythicStyledTooltip>
|
||||
</Tabs>
|
||||
</AppBar>
|
||||
<div hidden={value !== 0} style={{height: "100%"}} role='tabpanel' >
|
||||
<div hidden={value !== 0} style={{height: "100%", overflow: "auto"}} role='tabpanel' >
|
||||
{value === 0 &&
|
||||
<DisplayMedia agent_file_id={media?.agent_file_id || ""}
|
||||
task={task} filename={media?.filename || undefined}
|
||||
@@ -155,7 +164,7 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
expand={expand} />
|
||||
}
|
||||
</div>
|
||||
<div hidden={value !== 1} style={{height: "100%"}} role='tabpanel' >
|
||||
<div hidden={value !== 1} style={{height: "100%", overflow: "auto"}} role='tabpanel' >
|
||||
{value === 1 &&
|
||||
<DisplayText agent_file_id={media?.agent_file_id || ""}
|
||||
task={task} filename={media?.filename || undefined}
|
||||
@@ -163,7 +172,7 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
expand={expand} preview />
|
||||
}
|
||||
</div>
|
||||
<div hidden={value !== 2} style={{height: "100%"}} role='tabpanel' >
|
||||
<div hidden={value !== 2} style={{height: "100%", overflow: "auto"}} role='tabpanel' >
|
||||
{value === 2 &&
|
||||
<DisplayHex agent_file_id={media?.agent_file_id || ""}
|
||||
task={task} filename={media?.filename || undefined}
|
||||
@@ -172,6 +181,15 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
}
|
||||
|
||||
</div>
|
||||
<div hidden={value !== 3} style={{height: "100%", overflow: "auto"}} role='tabpanel' >
|
||||
{value === 3 &&
|
||||
<DisplayDatabase agent_file_id={media?.agent_file_id || ""}
|
||||
task={task} filename={media?.filename || undefined}
|
||||
fileMetaData={fileMetaData}
|
||||
expand={expand} />
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -353,7 +371,7 @@ export const DisplayMedia = ({agent_file_id, filename, expand, task, fileMetaDat
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const MaxRenderSize = 2000000;
|
||||
const MaxRenderSize = 2000000; // 2MB
|
||||
const DisplayFileMetaData = ({fileMetaData}) => {
|
||||
return (
|
||||
<Table style={{marginLeft: "0px", width: "100%", tableLayout: "fixed"}}>
|
||||
@@ -418,7 +436,10 @@ const DisplayText = ({agent_file_id, expand, filename, preview, fileMetaData}) =
|
||||
setLimitedPreviewWarning(true);
|
||||
}
|
||||
}else{
|
||||
snackActions.error(data.previewFile.error)
|
||||
console.log(data.previewFile.error);
|
||||
if(data.previewFile.error !== "EOF"){
|
||||
snackActions.error(data.previewFile.error);
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (data) => {
|
||||
@@ -478,7 +499,7 @@ const DisplayText = ({agent_file_id, expand, filename, preview, fileMetaData}) =
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [agent_file_id, filename]);
|
||||
}, [agent_file_id, filename, fileMetaData]);
|
||||
const onChangeMode = (event) => {
|
||||
setMode(event.target.value);
|
||||
}
|
||||
@@ -634,4 +655,255 @@ const DisplayHex = ({agent_file_id, expand, fileMetaData}) => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const MAX_ROWS = 1000;
|
||||
const DisplayDatabase = ({agent_file_id, expand, fileMetaData}) => {
|
||||
const theme = useTheme();
|
||||
const queryCountRef = React.useRef(1);
|
||||
const currentContentRef = React.useRef();
|
||||
const [selectedTab, setSelectedTab] = React.useState(0);
|
||||
const [sql, setSql] = React.useState(null);
|
||||
const [query, setQuery] = React.useState("select * from cookies;");
|
||||
const [results, setResults] = React.useState([]);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const onChangeText = (data) => {
|
||||
setQuery(data);
|
||||
}
|
||||
const removeQuery = (event, index) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if(selectedTab === index){
|
||||
if(index !== 0){
|
||||
setSelectedTab(index-1);
|
||||
}
|
||||
}
|
||||
setResults(results.toSpliced(index, 1));
|
||||
}
|
||||
const onSubmitQuery = (event) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
if(sql){
|
||||
try{
|
||||
const result = sql.exec(query);
|
||||
//console.log(result);
|
||||
if(result.length > 0){
|
||||
setResults([...results, {name: `Query: ${queryCountRef.current}`, query: query, results: result[0]}]);
|
||||
setSelectedTab(queryCountRef.current - 1);
|
||||
queryCountRef.current += 1;
|
||||
}
|
||||
}catch(e){
|
||||
console.log(e);
|
||||
snackActions.error(e.message);
|
||||
}
|
||||
} else {
|
||||
console.log(sql);
|
||||
snackActions.error("Failed to load as SQL Database");
|
||||
}
|
||||
}
|
||||
const onChangeTab = (_, v) => {
|
||||
const tabQuery = results[v]?.query;
|
||||
setQuery(tabQuery);
|
||||
setSelectedTab(v);
|
||||
}
|
||||
const onCopyToClipboard = (data) => {
|
||||
let result = copyStringToClipboard(data);
|
||||
if(result){
|
||||
snackActions.success("Copied text!");
|
||||
}else{
|
||||
snackActions.error("Failed to copy text");
|
||||
}
|
||||
}
|
||||
const onSaveOutputCSV = (result) => {
|
||||
const filteredData = result.results.values;
|
||||
let outputHeaders = "";
|
||||
let outputRow = "";
|
||||
outputHeaders = result.results.columns.join(",");
|
||||
for(let i = 0; i < filteredData.length; i++){
|
||||
outputRow += filteredData[i].join(",") + "\n"
|
||||
}
|
||||
onCopyToClipboard(outputHeaders + "\n" + outputRow);
|
||||
}
|
||||
const onSaveOutputPrettyPrint = (result) => {
|
||||
let tableData = result.results.values;
|
||||
if(tableData.length === 0){return}
|
||||
let outputHeaders = result.results.columns;
|
||||
let outputHeadersMaxLength = outputHeaders.reduce( (prev, cur) => {
|
||||
return [...prev, cur.length]
|
||||
}, []);
|
||||
tableData.map( cur => {
|
||||
for(let i = 0; i < outputHeaders.length; i++){
|
||||
if(outputHeadersMaxLength[i] < String(cur[i]).length){
|
||||
outputHeadersMaxLength[i] = String(cur[i]).length;
|
||||
}
|
||||
}
|
||||
});
|
||||
let allOutput = "";
|
||||
for(let i = 0; i < outputHeaders.length; i++){
|
||||
let remainingSpace = Math.max(0, outputHeadersMaxLength[i] - outputHeaders[i].length);
|
||||
allOutput += " " + outputHeaders[i].toUpperCase() + " ".repeat(remainingSpace + 2);
|
||||
}
|
||||
allOutput += "\n";
|
||||
for(let i = 0; i < outputHeaders.length; i++){
|
||||
let remainingSpace = Math.max(0, outputHeadersMaxLength[i] - outputHeaders[i].length);
|
||||
allOutput += " " + "-".repeat(outputHeaders[i].length) + " ".repeat(remainingSpace + 2);
|
||||
}
|
||||
allOutput += "\n";
|
||||
for(let i = 0; i < tableData.length; i++){
|
||||
for(let j = 0; j < outputHeaders.length; j++){
|
||||
let remainingSpace = Math.max(0, outputHeadersMaxLength[j] - String(tableData[i][j]).length);
|
||||
allOutput += "| " + String(tableData[i][j]) + " ".repeat(remainingSpace) + " ";
|
||||
}
|
||||
allOutput += " |\n"
|
||||
}
|
||||
onCopyToClipboard(allOutput);
|
||||
}
|
||||
React.useEffect( () => {
|
||||
async function initialize(){
|
||||
const newSQL = await initSQLJS({locateFile: () => sqlWasm});
|
||||
fetch('/direct/view/' + agent_file_id).then((response) => {
|
||||
if(response.status !== 200){
|
||||
setLoading(false);
|
||||
snackActions.warning("Failed to fetch contents from Mythic");
|
||||
return;
|
||||
}
|
||||
response.arrayBuffer().then( data => {
|
||||
const SQL = new newSQL.Database(new Uint8Array(data));
|
||||
if(SQL){
|
||||
setSql(SQL);
|
||||
}
|
||||
setLoading(false);
|
||||
}).catch(error => {
|
||||
setLoading(false);
|
||||
snackActions.warning("Error getting contents from server: " + error.toString());
|
||||
console.log("Error trying to get json response", error, response);
|
||||
});
|
||||
}).catch(error => {
|
||||
if(error.toString() === "TypeError: Failed to fetch"){
|
||||
snackActions.warning("Please refresh and accept the SSL connection error");
|
||||
} else {
|
||||
snackActions.warning("Error talking to server: " + error.toString());
|
||||
}
|
||||
setLoading(false);
|
||||
console.log("There was an error!", error);
|
||||
});
|
||||
}
|
||||
initialize();
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
// Execute query on Ctrl+Enter or Cmd+Enter when in the SQL editor
|
||||
if (e.shiftKey && e.key === 'Enter') {
|
||||
onSubmitQuery(e);
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [onSubmitQuery]);
|
||||
return (
|
||||
<div style={{display: "flex", height: "100%", flexDirection: "column", width: "100%", position: "relative"}}>
|
||||
<Backdrop open={loading} onClick={()=>{setLoading(false);}} style={{zIndex: 2, position: "absolute"}}>
|
||||
<CircularProgress color="inherit" disableShrink />
|
||||
</Backdrop>
|
||||
<div style={{display: "inline-flex", flexDirection: "row", alignItems: "center", borderBottom: "1px solid grey"}}>
|
||||
<AceEditor
|
||||
className={"roundedBottomCorners"}
|
||||
ref={currentContentRef}
|
||||
mode={'sql'}
|
||||
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
|
||||
fontSize={14}
|
||||
showGutter={ true}
|
||||
onChange={onChangeText}
|
||||
highlightActiveLine={false}
|
||||
showPrintMargin={false}
|
||||
value={query}
|
||||
height={"100%"}
|
||||
maxLines={3}
|
||||
width={"100%"}
|
||||
wrapEnabled={true}
|
||||
minLines={2}
|
||||
setOptions={{
|
||||
showLineNumbers: true,
|
||||
tabSize: 4,
|
||||
useWorker: false,
|
||||
showInvisibles: false,
|
||||
}}/>
|
||||
<Button color={"success"} variant={"contained"} onClick={onSubmitQuery} >Query</Button>
|
||||
</div>
|
||||
<TabContext style={{width: "100%", height: "100%", borderBottom: "1px solid grey", overflow: "auto"}} value={selectedTab}>
|
||||
<TabList onChange={onChangeTab} indicatorColor='secondary'
|
||||
textColor='primary'
|
||||
sx={{
|
||||
'& .Mui-selected': {
|
||||
color: "unset !important"
|
||||
}
|
||||
}}>
|
||||
{results.map((result, index) => (
|
||||
<Tab key={result.query + index} style={{padding: 0, paddingLeft: "10px", paddingRight: "10px"}} label={
|
||||
<div style={{display: "inline-block"}}>
|
||||
{result.name}
|
||||
<IconButton style={{padding: 0, marginLeft: "5px", marginRight: "5px", marginBottom: "5px"}}
|
||||
color={"error"}
|
||||
onClick={(e) => removeQuery(e, index)}>
|
||||
x
|
||||
</IconButton>
|
||||
</div>} value={index} className={selectedTab === index ? "selectedCallback" : ""} />
|
||||
))}
|
||||
</TabList>
|
||||
|
||||
{results.map((result, index) => (
|
||||
<div key={index} style={{display: selectedTab === index ? "flex" : "none",
|
||||
width: "100%", height: "100%", overflow: "auto",
|
||||
flexDirection: "column"}}>
|
||||
{result?.results?.values?.length >= MAX_ROWS &&
|
||||
<Paper key={"overflow" + index} style={{display: selectedTab === index ? "flex" : "none",
|
||||
justifyContent: "center", hidden: selectedTab !== index}}>
|
||||
<WarningOutlinedIcon color={"warning"}></WarningOutlinedIcon>
|
||||
{"Output is truncated to first " + MAX_ROWS + " rows"}
|
||||
<WarningOutlinedIcon color={"warning"}></WarningOutlinedIcon>
|
||||
</Paper>
|
||||
}
|
||||
<div key={"output" + index} style={{display: selectedTab === index ? "flex" : "none", alignItems: "center", width: "100%"}}>
|
||||
{"Save Output As: "}
|
||||
<Button variant={"outlined"} style={{marginLeft: "10px"}} onClick={() => onSaveOutputCSV(result)} >CSV</Button>
|
||||
<Button variant={"outlined"} style={{marginLeft: "10px"}} onClick={() => onSaveOutputPrettyPrint(result)} >Pretty Print</Button>
|
||||
</div>
|
||||
<TabPanel value={index} key={"tabpanel" + index}
|
||||
style={{padding: 0, height: "100%", width: "100%", overflow: "auto", position: "relative",
|
||||
display: selectedTab === index ? "flex" : "none",
|
||||
flexDirection: "column"}} >
|
||||
<DisplayDatabaseResult result={result?.results} />
|
||||
</TabPanel>
|
||||
</div>
|
||||
))}
|
||||
</TabContext>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const DisplayDatabaseResult = ({result}) => {
|
||||
const [columns, setColumns] = React.useState(result.columns);
|
||||
const [rows, setRows] = React.useState(result.values?.slice(0, MAX_ROWS));
|
||||
return (
|
||||
<Table stickyHeader style={{width: "100%", height: "100%", position: "absolute"}}>
|
||||
<TableHead >
|
||||
<TableRow>
|
||||
{columns.map((column, index) => (
|
||||
<TableCell key={"header" + index}>{column}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row, index1) => (
|
||||
<TableRow key={"row" + index1} hover>
|
||||
{columns.map((_, index2) => (
|
||||
<TableCell key={"row" + index1+ "cell" + index2}>
|
||||
{row[index2]}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import WrapTextIcon from '@mui/icons-material/WrapText';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import { IconButton } from '@mui/material';
|
||||
import { IconButton, Select } from '@mui/material';
|
||||
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
|
||||
import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
|
||||
import { useReactiveVar } from '@apollo/client';
|
||||
@@ -16,6 +16,7 @@ import { meState } from '../../../cache';
|
||||
import CodeIcon from '@mui/icons-material/Code';
|
||||
import {GetOutputFormatAll} from "./ResponseDisplayInteractive";
|
||||
import PaletteIcon from '@mui/icons-material/Palette';
|
||||
import Input from '@mui/material/Input';
|
||||
|
||||
const MaxRenderSize = 2000000;
|
||||
export const ResponseDisplayPlaintext = (props) =>{
|
||||
@@ -96,14 +97,25 @@ export const ResponseDisplayPlaintext = (props) =>{
|
||||
return (
|
||||
<div style={{display: "flex", height: "100%", flexDirection: "column"}}>
|
||||
{showOptions &&
|
||||
<div style={{display: "inline-flex", flexDirection: "row", alignItems: "center"}}>
|
||||
<FormControl sx={{ display: "inline-block", marginLeft: "10px" }} size="small">
|
||||
<div style={{display: "inline-flex", flexDirection: "row", alignItems: "center",
|
||||
color: theme.outputTextColor,
|
||||
backgroundColor: theme.outputBackgroundColor + (theme.palette.mode === 'dark' ? "D0" : "20"),}}>
|
||||
<FormControl sx={{ display: "inline-block", marginLeft: "10px", color: theme.outputTextColor, }} size="small">
|
||||
<TextField
|
||||
label={"Syntax"}
|
||||
select
|
||||
margin={"dense"}
|
||||
size={"small"}
|
||||
style={{display: "inline-block", width: "100%"}}
|
||||
sx={{
|
||||
// Target the icon element directly within the Select
|
||||
'.MuiSelect-icon': {
|
||||
color: theme.outputTextColor, // Set your desired color
|
||||
},
|
||||
'.MuiInputBase-root': {
|
||||
color: theme.outputTextColor,
|
||||
}
|
||||
}}
|
||||
style={{display: "inline-block", width: "100%",}}
|
||||
value={mode}
|
||||
onChange={onChangeMode}
|
||||
>
|
||||
@@ -139,7 +151,6 @@ export const ResponseDisplayPlaintext = (props) =>{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
zIndex: 1,
|
||||
backgroundColor: theme.palette.secondary.main
|
||||
}}>
|
||||
{showOptions &&
|
||||
<UnfoldLessIcon onClick={onChangeShowOptions}
|
||||
@@ -189,7 +200,6 @@ export const ResponseDisplayPlaintext = (props) =>{
|
||||
//autoScrollEditorIntoView={true}
|
||||
wrapEnabled={props.displayType !== 'console' ? wrapText : true}
|
||||
minLines={1}
|
||||
//maxLines={props.expand ? 50 : 20}
|
||||
setOptions={{
|
||||
showLineNumbers: props.displayType !== 'console',
|
||||
tabSize: 4,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, {useEffect, useRef} from 'react';
|
||||
import {Button} from '@mui/material';
|
||||
import { MythicViewJSONAsTableDialog, MythicDialog } from '../../MythicComponents/MythicDialog';
|
||||
import {MythicViewJSONAsTableDialog, MythicDialog, MythicModifyStringDialog} from '../../MythicComponents/MythicDialog';
|
||||
import { MythicDisplayTextDialog } from '../../MythicComponents/MythicDisplayTextDialog';
|
||||
import { ResponseDisplayTableDialogTable } from './ResponseDisplayTableDialogTable';
|
||||
import Paper from '@mui/material/Paper';
|
||||
@@ -21,6 +21,7 @@ import {faList, faTrashAlt, faSkullCrossbones, faCamera, faSyringe, faFolder, fa
|
||||
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);
|
||||
@@ -88,7 +89,7 @@ export const getIconName = (iconName) => {
|
||||
case "camera":
|
||||
return faCamera;
|
||||
default:
|
||||
return faFileAlt;
|
||||
return iconName;
|
||||
}
|
||||
}
|
||||
export const getIconColor = (theme, color) => {
|
||||
@@ -359,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 || " "}
|
||||
@@ -407,11 +408,12 @@ const ResponseDisplayTableActionCellButton = ({cellData, callback_id}) => {
|
||||
table={taskingData?.value || {}} callback_id={callback_id} onClose={finishedViewingData} />}
|
||||
/>
|
||||
}
|
||||
<Button size="small" variant={"contained"} 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}
|
||||
@@ -568,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"});
|
||||
@@ -583,9 +585,9 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
});
|
||||
}, [sortedData, table?.headers, callback_id]
|
||||
);
|
||||
const onSubmitFilterOptions = (newFilterOptions) => {
|
||||
setFilterOptions(newFilterOptions);
|
||||
}
|
||||
const onSubmitFilterOptions = (value) => {
|
||||
setFilterOptions({...filterOptions, [selectedColumn.key]: value });
|
||||
}
|
||||
|
||||
const filterOutButtonsFromRowData = (data) => {
|
||||
let rowData = {};
|
||||
@@ -664,7 +666,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
];
|
||||
const contextMenuOptions = [
|
||||
{
|
||||
name: 'Filter Column',
|
||||
name: 'Filter Column', type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -680,7 +682,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Copy Table as JSON",
|
||||
name: "Copy Table as JSON", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -689,7 +691,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Copy Table as CSV",
|
||||
name: "Copy Table as CSV", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -720,7 +722,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Copy Table as TSV",
|
||||
name: "Copy Table as TSV", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -751,7 +753,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Copy Table as PrettyPrint Text",
|
||||
name: "Copy Table as PrettyPrint Text", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -799,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 (
|
||||
@@ -829,11 +831,16 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={<TableFilterDialog
|
||||
selectedColumn={selectedColumn}
|
||||
filterOptions={filterOptions}
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
onClose={()=>{setOpenContextMenu(false);}} />}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={filterOptions[selectedColumn.key]}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import React from 'react';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import {a11yProps} from "../../MythicComponents/MythicTabPanel";
|
||||
import {ResponseDisplayBrowserScriptComponent} from "./ResponseDisplay";
|
||||
|
||||
function ResponseDisplayTabsLabel(props) {
|
||||
const { label, index, ...other } =
|
||||
props;
|
||||
return (
|
||||
<Tab
|
||||
label={
|
||||
<span>
|
||||
{label}
|
||||
</span>
|
||||
}
|
||||
sx={{
|
||||
'&.Mui-selected': {
|
||||
border: "2px solid grey",
|
||||
borderRadius: "4px",
|
||||
//backgroundColor: 'info.main', // Color when selected
|
||||
},
|
||||
}}
|
||||
wrapped={true}
|
||||
{...a11yProps(index)}
|
||||
{...other}
|
||||
/>
|
||||
);
|
||||
}
|
||||
function ResponseDisplayTabsPanel(props) {
|
||||
const { children, value, index, ...other } =
|
||||
props;
|
||||
const style =
|
||||
props.style === undefined
|
||||
? {
|
||||
display: value === index ? 'flex' : 'none',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
overflowY: "auto",
|
||||
}
|
||||
: props.style;
|
||||
return (
|
||||
<div
|
||||
role='tabpanel'
|
||||
hidden={value !== index}
|
||||
id={`scrollable-auto-tabpanel-${index}`}
|
||||
aria-labelledby={`scrollable-auto-tab-${index}`}
|
||||
style={style}
|
||||
{...other}>
|
||||
{<React.Fragment>{children}</React.Fragment>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export function ResponseDisplayTabs({ tabs, task, expand, displayType, output }) {
|
||||
const mountedRef = React.useRef(true);
|
||||
const [value, setValue] = React.useState(0);
|
||||
const handleChange = (event, newValue) => {
|
||||
setValue(newValue);
|
||||
};
|
||||
React.useEffect( () => {
|
||||
return() => {
|
||||
mountedRef.current = false;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div style={{width: "100%", maxWidth: "100%", display: 'flex', flexDirection: 'column', flexGrow: 1, height: expand ? "100%" : "400px", }}>
|
||||
<AppBar color='default' position={"static"} style={{maxHeight: "25%", overflowY: 'auto', maxWidth: "100%", flexWrap: "no-wrap"}} >
|
||||
<Tabs
|
||||
value={value}
|
||||
variant={"standard"}
|
||||
onChange={handleChange}
|
||||
indicatorColor='primary'
|
||||
textColor='primary'
|
||||
style={{ }}
|
||||
TabIndicatorProps={{style: {
|
||||
display: "none",
|
||||
}}}
|
||||
aria-label='scrollable tabs'>
|
||||
{tabs.map((tab, index) => (
|
||||
<ResponseDisplayTabsLabel
|
||||
key={'tablabel' + task.id + index}
|
||||
index={index}
|
||||
label={tab.title}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</AppBar>
|
||||
{tabs.map((tab, index) => (
|
||||
<ResponseDisplayTabsPanel
|
||||
key={'tabpanel' + task.id + index}
|
||||
value={value}
|
||||
index={index}>
|
||||
<ResponseDisplayBrowserScriptComponent
|
||||
task={task} expand={expand} displayType={displayType} output={output}
|
||||
browserScriptData={tab.content}
|
||||
/>
|
||||
</ResponseDisplayTabsPanel>
|
||||
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {operatorSettingDefaults} from "../../../cache";
|
||||
import {TaskFromUIButton} from "./TaskFromUIButton";
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {faSkullCrossbones} from '@fortawesome/free-solid-svg-icons';
|
||||
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
||||
// Icons for console-view display
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import PersonOutlineIcon from '@mui/icons-material/PersonOutline';
|
||||
@@ -139,19 +140,21 @@ export const StyledAccordionSummary = styled(AccordionSummary)((
|
||||
) => ({
|
||||
[`&.${accordionClasses.root}`]: {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
paddingLeft: 0,
|
||||
//paddingRight: 0,
|
||||
height: "auto",
|
||||
width: "100%",
|
||||
whiteSpace: "break-all",
|
||||
wordBreak: "break-all",
|
||||
userSelect: "text",
|
||||
boxShadow: "unset",
|
||||
backgroundColor: "unset",
|
||||
justifyContent: "flex-start",
|
||||
},
|
||||
[`& .${accordionClasses.content}`]: {
|
||||
margin: 0,
|
||||
height: "100%",
|
||||
padding: 0,
|
||||
width: "inherit",
|
||||
},
|
||||
[`& .${accordionClasses.expandIcon}`]: {
|
||||
margin: 0,
|
||||
@@ -224,7 +227,7 @@ const ColoredTaskDisplay = ({task, theme, children, expanded}) => {
|
||||
setThemeColor(theme.palette.warning.main);
|
||||
}else if(task.status.toLowerCase().includes("processing")){
|
||||
setThemeColor(theme.palette.warning.main);
|
||||
}else if(task.status === "completed" || (task.status === "success" && task.completed)){
|
||||
}else if(task.status === "completed" || task.completed){
|
||||
setThemeColor(theme.palette.success.main);
|
||||
}else{
|
||||
setThemeColor(theme.palette.info.main);
|
||||
@@ -232,7 +235,7 @@ const ColoredTaskDisplay = ({task, theme, children, expanded}) => {
|
||||
}, [task.status, task.completed])
|
||||
return(
|
||||
<span style={{display: "flex", margin: 0, borderWidth: 0, padding: 0, minHeight: "48px", alignItems: "center",
|
||||
height: "100%", borderLeft: "6px solid " + themeColor, paddingLeft: "5px", width: "100%",
|
||||
height: "100%", borderLeft: "6px solid " + themeColor, paddingLeft: "5px", width: "100%", maxWidth: "100%",
|
||||
borderTopLeftRadius: "4px", borderBottomLeftRadius: expanded ? 0 : "4px"}}>
|
||||
{children}
|
||||
</span>
|
||||
@@ -242,7 +245,7 @@ const GetOperatorDisplay = ({initialHideUsernameValue, task}) => {
|
||||
if(initialHideUsernameValue){
|
||||
return '';
|
||||
}
|
||||
return task.operator.username;
|
||||
return " / " + task.operator.username;
|
||||
}
|
||||
export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayChildren, toggleDisplayChildren, expanded }) => {
|
||||
const [displayComment, setDisplayComment] = React.useState(false);
|
||||
@@ -253,6 +256,7 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh
|
||||
const initialShowHostnameValue = GetMythicSetting({setting_name: "showHostname", default_value: operatorSettingDefaults.showHostname});
|
||||
const initialShowCallbackGroupsValue = GetMythicSetting({setting_name: "showCallbackGroups", default_value: operatorSettingDefaults.showCallbackGroups});
|
||||
const initialTaskTimestampDisplayField = GetMythicSetting({setting_name: "taskTimestampDisplayField", default_value: operatorSettingDefaults.taskTimestampDisplayField});
|
||||
const initialShowOPSECBypassUsername = GetMythicSetting({setting_name: "showOPSECBypassUsername", default_value: operatorSettingDefaults.showOPSECBypassUsername});
|
||||
const displayTimestamp = task[initialTaskTimestampDisplayField] ? task[initialTaskTimestampDisplayField] : task.timestamp;
|
||||
const [openKillTaskButton, setOpenKillTaskButton] = React.useState({open: false});
|
||||
const toggleDisplayComment = (evt) => {
|
||||
@@ -267,7 +271,6 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh
|
||||
if(onClick){
|
||||
onClick(e);
|
||||
}
|
||||
//preventPropagation(e);
|
||||
}
|
||||
const onClickKillIcon = (e, open) => {
|
||||
if(e){
|
||||
@@ -276,7 +279,7 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh
|
||||
setOpenKillTaskButton({open: open});
|
||||
}
|
||||
return (
|
||||
<ColoredTaskDisplay task={task} theme={theme} expanded={expanded} >
|
||||
<ColoredTaskDisplay task={task} theme={theme} expanded={expanded} >
|
||||
<div id={taskDivID} style={{width: "100%"}}>
|
||||
{displayComment && (
|
||||
<React.Fragment>
|
||||
@@ -284,80 +287,92 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh
|
||||
<Typography className={classes.heading} onClick={preventPropagation}>{task.comment}</Typography>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<div style={{lineHeight: 0}}>
|
||||
<div style={{lineHeight: 0}} onClick={onLocalClick}>
|
||||
<Typography className={classes.taskAndTimeDisplay} onClick={preventPropagation}>
|
||||
[{toLocalTime(displayTimestamp, me?.user?.view_utc_time || false)}]
|
||||
{" / "}
|
||||
<span style={{}}>
|
||||
{task.has_intercepted_response &&
|
||||
<>
|
||||
<MythicStyledTooltip
|
||||
title={"This task has responses that have been intercepted and changed due to a workflow container"}>
|
||||
<IconButton style={{padding: 0}} color={"secondary"}>
|
||||
<CropRotateTwoToneIcon fontSize={"small"}/>
|
||||
</IconButton>
|
||||
|
||||
</MythicStyledTooltip>
|
||||
{"/ "}
|
||||
</>
|
||||
}
|
||||
{task?.eventstepinstance !== null &&
|
||||
<>
|
||||
<MythicStyledTooltip title={"Task created via Eventing, click to view entire event flow in separate page"} >
|
||||
<IconButton component={Link} href={'/new/eventing?eventgroup=' +
|
||||
task?.eventstepinstance?.eventgroupinstance?.eventgroup?.id +
|
||||
"&eventgroupinstance=" + task?.eventstepinstance?.eventgroupinstance?.id
|
||||
} target={"_blank"} style={{padding: 0}}
|
||||
color="inherit" disableFocusRipple={true}
|
||||
{task.has_intercepted_response &&
|
||||
<span style={{display:"inline-flex", whiteSpace: "pre-wrap"}}>
|
||||
<MythicStyledTooltip
|
||||
title={"This task has responses that have been intercepted and changed due to a workflow container"}>
|
||||
<IconButton color={"inherit"} style={{padding: 0}} disableFocusRipple={true}
|
||||
disableRipple={true}>
|
||||
<PlayCircleFilledTwoToneIcon />
|
||||
<CropRotateTwoToneIcon style={{height: "15px", cursor: "default"}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
|
||||
{"/ "}
|
||||
</>
|
||||
}
|
||||
<MythicStyledTooltip title={"View Task in separate page"} >
|
||||
<Link style={{wordBreak: "break-all", color: theme.taskPromptTextColor,}} underline={"always"} target={"_blank"}
|
||||
href={"/new/task/" + task.display_id}>T-{task.display_id}</Link>
|
||||
</MythicStyledTooltip>
|
||||
{" / "}
|
||||
</span>
|
||||
}
|
||||
{task?.eventstepinstance !== null &&
|
||||
<span style={{display:"inline-flex", whiteSpace: "pre-wrap"}}>
|
||||
<MythicStyledTooltip title={"Task created via Eventing, click to view entire event flow in separate page"} >
|
||||
<IconButton component={Link} href={'/new/eventing?eventgroup=' +
|
||||
task?.eventstepinstance?.eventgroupinstance?.eventgroup?.id +
|
||||
"&eventgroupinstance=" + task?.eventstepinstance?.eventgroupinstance?.id
|
||||
} target={"_blank"} style={{padding: 0, }}
|
||||
color="inherit" disableFocusRipple={true}
|
||||
disableRipple={true}>
|
||||
<PlayCircleFilledTwoToneIcon fontSize={"small"} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
|
||||
{" / "}
|
||||
</span>
|
||||
}
|
||||
<span style={{display:"inline-flex", whiteSpace: "pre-wrap"}}>
|
||||
<MythicStyledTooltip title={"View Task in separate page"} >
|
||||
<Link style={{wordBreak: "break-all", color: theme.taskPromptTextColor,}} underline={"always"} target={"_blank"}
|
||||
href={"/new/task/" + task.display_id}>T-{task.display_id}</Link>
|
||||
</MythicStyledTooltip>
|
||||
{!task.completed && task.status_timestamp_processing &&
|
||||
<>
|
||||
<MythicStyledTooltip title={"Task the agent to kill this task"} >
|
||||
<FontAwesomeIcon size={"sm"} icon={faSkullCrossbones} onClick={(e) => onClickKillIcon(e, true)}
|
||||
style={{cursor: "pointer", height: "15px", marginLeft: "5px"}} />
|
||||
</MythicStyledTooltip>
|
||||
</>
|
||||
|
||||
<MythicStyledTooltip title={"Task the agent to kill this task"} >
|
||||
<FontAwesomeIcon size={"sm"} icon={faSkullCrossbones} onClick={(e) => onClickKillIcon(e, true)}
|
||||
style={{cursor: "pointer", height: "12px", marginLeft: "5px"}} />
|
||||
</MythicStyledTooltip>
|
||||
|
||||
}
|
||||
</span>
|
||||
{" / "}
|
||||
<GetOperatorDisplay initialHideUsernameValue={initialHideUsernameValue} task={task}/>
|
||||
{" / "}
|
||||
<MythicStyledTooltip title={"View Callback in separate page"}>
|
||||
<Link style={{wordBreak: "break-all", color: theme.taskPromptTextColor}} underline="always" target="_blank"
|
||||
href={"/new/callbacks/" + task.callback.display_id}>C-{task.callback.display_id}</Link>
|
||||
</MythicStyledTooltip>
|
||||
|
||||
{initialShowHostnameValue ? ` / ${task.callback.host} ` : ''}
|
||||
{initialShowIPValue ? `/ ${ipValue} ` : ''}
|
||||
{initialShowCallbackGroupsValue ? `/ ${task.callback.mythictree_groups.join(', ')} ` : ''}
|
||||
{" / "}
|
||||
{task?.command?.payloadtype?.name}
|
||||
{" / "}
|
||||
{initialShowOPSECBypassUsername && <>
|
||||
{(task?.opsec_pre_bypass_user?.username || task?.opsec_post_bypass_user?.username) && <span
|
||||
style={{display:"inline-flex", whiteSpace: "pre-wrap"}}>
|
||||
{" /"}
|
||||
<MythicStyledTooltip title={"The specified usernames approved OPSEC bypasses for this task"}
|
||||
tooltipStyle={{padding: 0}}>
|
||||
<LockOpenIcon color={"inherit"} style={{height: "15px"}}/>
|
||||
</MythicStyledTooltip>
|
||||
{task?.opsec_pre_bypass_user?.username}
|
||||
{task?.opsec_pre_bypass_user?.username && task?.opsec_post_bypass_user?.username && ", "}
|
||||
{task?.opsec_post_bypass_user?.username}
|
||||
</span>}
|
||||
</>}
|
||||
{" "}
|
||||
<TaskStatusDisplay task={task} theme={theme}/>
|
||||
{task.comment.length > 0 ? (
|
||||
{task.comment.length > 0 &&
|
||||
<span className={classes.column}>
|
||||
{" / "}
|
||||
<IconButton size="small" style={{padding: "0"}}
|
||||
onClick={toggleDisplayComment}><ChatOutlinedIcon fontSize={"small"}/></IconButton>
|
||||
</span>
|
||||
) : null}
|
||||
}
|
||||
{task.comment}
|
||||
</Typography>
|
||||
</Typography>
|
||||
<TaskTagDisplay task={task}/>
|
||||
</div>
|
||||
<div>
|
||||
<div onClick={onLocalClick}>
|
||||
|
||||
<div className={classes.column} onClick={onLocalClick}>
|
||||
<div className={classes.column} >
|
||||
<Badge badgeContent={alertBadges} color="warning" anchorOrigin={{vertical: 'top', horizontal: 'left'}}>
|
||||
{task.tasks.length > 0 && !displayChildren &&
|
||||
<ExpandMoreIcon onClick={toggleDisplayChildren} />
|
||||
@@ -425,7 +440,7 @@ const TaskRow = ({task, filterOptions, me, newlyIssuedTasks, indentLevel, collap
|
||||
"parameterString": onlyParameters,
|
||||
"hideErrors": hideErrors
|
||||
}); */
|
||||
if(hideBrowserTasking && task.tasking_location.includes("browser")){
|
||||
if(hideBrowserTasking && task.tasking_location.includes("_browser")){
|
||||
setShouldDisplay(false);
|
||||
return;
|
||||
}
|
||||
@@ -497,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) => {
|
||||
@@ -504,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){
|
||||
@@ -659,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}
|
||||
/>
|
||||
@@ -781,7 +807,7 @@ export const TaskLabelFlat = ({task, me, showOnSelectTask, onSelectTask, graphVi
|
||||
}
|
||||
|
||||
return(
|
||||
<StyledPaper className={task.selected ? classes.root + " selectedTask no-box-shadow" : classes.root}
|
||||
<StyledPaper className={task.selected && showOnSelectTask ? classes.root + " selectedTask no-box-shadow" : classes.root}
|
||||
elevation={5} style={{marginRight: 0, cursor: "pointer"}} id={`taskHeader-${task.id}`}
|
||||
onClick={onClickEntry}
|
||||
>
|
||||
|
||||
@@ -36,6 +36,11 @@ import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen';
|
||||
import CodeOffIcon from '@mui/icons-material/CodeOff';
|
||||
import SettingsTwoToneIcon from '@mui/icons-material/SettingsTwoTone';
|
||||
import {b64DecodeUnicode} from './ResponseDisplay';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {EventTriggerContextSelectDialog} from "../Eventing/EventTriggerContextSelect";
|
||||
import PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwoTone';
|
||||
|
||||
const ReissueTaskMutationGQL = gql`
|
||||
mutation reissueTaskMutation($task_id: Int!){
|
||||
@@ -131,7 +136,7 @@ export const TaskDisplayContainerFlat = ({task, me}) => {
|
||||
}, [searchOutput]);
|
||||
|
||||
return (
|
||||
<div style={{ height: "100%", position: "relative", display: "flex", flexDirection: "column", overflowY: "auto", }}>
|
||||
<div style={{ height: "100%", position: "relative", display: "flex", overflowY: "auto", justifyContent: "flex-end" }}>
|
||||
<ResponseDisplay
|
||||
key={task.id}
|
||||
task={task}
|
||||
@@ -143,7 +148,7 @@ export const TaskDisplayContainerFlat = ({task, me}) => {
|
||||
expand={true}
|
||||
responseRef={responseRef}
|
||||
/>
|
||||
<SpeedDialDisplayGeneric toggleViewBrowserScript={toggleViewBrowserScript}
|
||||
<SideDisplayGeneric toggleViewBrowserScript={toggleViewBrowserScript}
|
||||
toggleSelectAllOutput={toggleSelectAllOutput}
|
||||
toggleOpenSearch={toggleOpenSearch}
|
||||
taskData={task}
|
||||
@@ -179,11 +184,278 @@ export const TaskDisplayContainerConsole = ({task, me}) => {
|
||||
);
|
||||
}
|
||||
|
||||
const SideDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput,
|
||||
toggleOpenSearch, taskData, viewAllOutput, me,
|
||||
responseRef, viewBrowserScript}) => {
|
||||
const theme = useTheme();
|
||||
const [task, setTask] = React.useState(taskData || {});
|
||||
const eventingDataRef = React.useRef({name: "", value: 0});
|
||||
const [openEventingDialog, setOpenEventingDialog] = React.useState(false);
|
||||
const [openTaskTagDialog, setOpenTaskTagDialog] = React.useState(false);
|
||||
const [openCommentDialog, setOpenCommentDialog] = React.useState(false);
|
||||
const [openParametersDialog, setOpenParametersDialog] = React.useState(false);
|
||||
const [openTokenDialog, setOpenTokenDialog] = React.useState(false);
|
||||
const [openStdoutStderrDialog, setOpenStdoutStderrDialog] = React.useState(false);
|
||||
const [openOpsecDialog, setOpenOpsecDialog] = React.useState({open: false, view: "pre"});
|
||||
const [downloadResponses] = useLazyQuery(getAllResponsesLazyQuery, {
|
||||
fetchPolicy: "network-only",
|
||||
onCompleted: (data) => {
|
||||
const output = data.response.reduce( (prev, cur) => {
|
||||
return prev + b64DecodeUnicode(cur.response);
|
||||
}, b64DecodeUnicode(""));
|
||||
downloadFileFromMemory(output, "task_" + task.id + ".txt");
|
||||
},
|
||||
onError: (data) => {
|
||||
|
||||
}
|
||||
});
|
||||
React.useEffect( () => {
|
||||
setTask(taskData);
|
||||
}, [taskData.id, taskData.token, taskData.original_params, taskData.opsec_pre_blocked, taskData.opsec_pre_bypassed, taskData.opsec_post_blocked, taskData.opsec_post_bypassed])
|
||||
const onDownloadResponses = () => {
|
||||
downloadResponses({variables: {task_id: task.id}});
|
||||
};
|
||||
const copyToClipboard = () => {
|
||||
let command = task?.command?.cmd || task.command_name;
|
||||
let result = copyStringToClipboard(command + " " + task.original_params);
|
||||
if(result){
|
||||
snackActions.success("Copied text!");
|
||||
}else{
|
||||
snackActions.error("Failed to copy text");
|
||||
}
|
||||
};
|
||||
const [reissueTask] = useMutation(ReissueTaskMutationGQL, {
|
||||
onCompleted: data => {
|
||||
if(data.reissue_task.status === "success"){
|
||||
snackActions.success("Successfully re-issued task to Mythic");
|
||||
}else{
|
||||
snackActions.error("Failed to re-issue task to Mythic: " + data.reissue_task.error);
|
||||
}
|
||||
},
|
||||
onError: data => {
|
||||
console.log(data);
|
||||
snackActions.error("Failed to re-issue task: " + data);
|
||||
}
|
||||
});
|
||||
const [reissueTaskHandler] = useMutation(ReissueTaskHandlerMutationGQL, {
|
||||
onCompleted: data => {
|
||||
if(data.reissue_task_handler.status === "success"){
|
||||
snackActions.success("Successfully resubmitted task for handling");
|
||||
}else{
|
||||
snackActions.warning("Failed to resubmit task for handling: " + data.reissue_task_handler.error);
|
||||
}
|
||||
|
||||
},
|
||||
onError: data => {
|
||||
console.log(data);
|
||||
snackActions.error("Error resubmitting task for handling: " + data);
|
||||
}
|
||||
});
|
||||
const onDownloadImageClickPng = () => {
|
||||
// we calculate a transform for the nodes so that all nodes are visible
|
||||
// we then overwrite the transform of the `.react-flow__viewport` element
|
||||
// with the style option of the html-to-image library
|
||||
snackActions.info("Saving image to png...");
|
||||
(async () => {
|
||||
const canvas = await html2canvas(responseRef.current);
|
||||
const image = canvas.toDataURL("image/png", 1.0);
|
||||
const fakeLink = window.document.createElement("a");
|
||||
fakeLink.style = "display:none;";
|
||||
fakeLink.download = "task_output.png";
|
||||
|
||||
fakeLink.href = image;
|
||||
|
||||
document.body.appendChild(fakeLink);
|
||||
fakeLink.click();
|
||||
document.body.removeChild(fakeLink);
|
||||
|
||||
fakeLink.remove();
|
||||
|
||||
})();
|
||||
};
|
||||
const onReissueTask = () => {
|
||||
reissueTask({variables: {task_id: task.id}});
|
||||
}
|
||||
const onReissueTaskHandler = () => {
|
||||
reissueTaskHandler({variables: {task_id: task.id}});
|
||||
}
|
||||
const onTriggerEventing = () => {
|
||||
eventingDataRef.current = {
|
||||
name: "task_id",
|
||||
value: task.id
|
||||
};
|
||||
setOpenEventingDialog(true);
|
||||
}
|
||||
return (
|
||||
<div style={{height: "100%"}}>
|
||||
{openTaskTagDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openTaskTagDialog}
|
||||
onClose={()=>{setOpenTaskTagDialog(false);}}
|
||||
innerDialog={<ViewEditTagsDialog me={me} target_object={"task_id"} target_object_id={task.id} onClose={()=>{setOpenTaskTagDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
{openCommentDialog && <MythicDialog fullWidth={true} maxWidth="md" open={openCommentDialog}
|
||||
onClose={()=>{setOpenCommentDialog(false);}}
|
||||
innerDialog={<TaskCommentDialog task_id={task.id} onClose={()=>{setOpenCommentDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
{openParametersDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openParametersDialog}
|
||||
onClose={()=>{setOpenParametersDialog(false);}}
|
||||
innerDialog={<TaskViewParametersDialog task_id={task.id} onClose={()=>{setOpenParametersDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
{openTokenDialog && <MythicDialog fullWidth={true} maxWidth="md" open={openTokenDialog}
|
||||
onClose={()=>{setOpenTokenDialog(false);}}
|
||||
innerDialog={<TaskTokenDialog token_id={task.token === undefined ? 0 : task.token.id} onClose={()=>{setOpenTokenDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
{openOpsecDialog.open && <MythicDialog fullWidth={true} maxWidth="lg" open={openOpsecDialog.open}
|
||||
onClose={()=>{setOpenOpsecDialog({...openOpsecDialog, open: false});}}
|
||||
innerDialog={<TaskOpsecDialog task_id={task.id} view={openOpsecDialog.view} onClose={()=>{setOpenOpsecDialog({...openOpsecDialog, open: false});}} />}
|
||||
/>
|
||||
}
|
||||
|
||||
{openStdoutStderrDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openStdoutStderrDialog}
|
||||
onClose={()=>{setOpenStdoutStderrDialog(false);}}
|
||||
innerDialog={<TaskViewStdoutStderrDialog task_id={task.id} onClose={()=>{setOpenStdoutStderrDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
{openEventingDialog &&
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
maxWidth="xl"
|
||||
open={openEventingDialog}
|
||||
onClose={() => {setOpenEventingDialog(false);}}
|
||||
innerDialog={
|
||||
<EventTriggerContextSelectDialog onClose={() => {setOpenEventingDialog(false);}}
|
||||
triggerContext={eventingDataRef.current}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
<Paper elevation={5} style={{width: "30px", display: "flex", flexDirection: "column", alignItems: "center", overflow: "hidden",
|
||||
backgroundColor: "transparent", height: "100%"}}>
|
||||
<MythicStyledTooltip title={"Toggle BrowserScript"} >
|
||||
<IconButton onClick={toggleViewBrowserScript} style={{paddingBottom: "3px"}}>
|
||||
{viewBrowserScript ? <CodeOffIcon color={"error"} /> : <CodeIcon color={"success"}/>}
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={viewAllOutput ? "View Paginated Output" : "View All Output"}>
|
||||
<IconButton onClick={toggleSelectAllOutput} style={{paddingBottom: "3px"}}>
|
||||
{viewAllOutput ? <CloseFullscreenIcon color={"error"} /> : <FontAwesomeIcon style={{color: theme.palette.success.main}} icon={faExpandArrowsAlt} size="sm" />}
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Search Output"}>
|
||||
<IconButton onClick={toggleOpenSearch} style={{paddingBottom: "3px"}}>
|
||||
<SearchIcon color={"info"} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Download output"}>
|
||||
<IconButton onClick={onDownloadResponses} style={{paddingBottom: "3px"}}>
|
||||
<GetAppIcon color={"success"}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Download screenshot of output"}>
|
||||
<IconButton onClick={onDownloadImageClickPng} style={{paddingBottom: "3px"}}>
|
||||
<InsertPhotoIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Edit Tags"}>
|
||||
<IconButton onClick={()=>{setOpenTaskTagDialog(true)}} style={{paddingBottom: "3px"}}>
|
||||
<LocalOfferOutlinedIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Open Task in New Window"}>
|
||||
<IconButton onClick={()=> {window.open('/new/task/' + task.display_id, "_blank")}} style={{paddingBottom: "3px"}}>
|
||||
<FontAwesomeIcon icon={faExternalLinkAlt} size="sm" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Copy original params to clipboard"}>
|
||||
<IconButton onClick={copyToClipboard} style={{paddingBottom: "3px"}}>
|
||||
<FileCopyOutlinedIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Edit Comment"}>
|
||||
<IconButton onClick={()=>{setOpenCommentDialog(true)}} style={{paddingBottom: "3px"}}>
|
||||
<RateReviewOutlinedIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"View All Parameters And Timestamps"}>
|
||||
<IconButton onClick={()=>{setOpenParametersDialog(true);}} style={{paddingBottom: "3px"}}>
|
||||
<KeyboardIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"View Stdout/Stderr of Task"}>
|
||||
<IconButton onClick={()=>{setOpenStdoutStderrDialog(true);}} style={{paddingBottom: "3px"}}>
|
||||
<FontAwesomeIcon style={{color: theme.palette.error.main}} icon={faExclamationTriangle} size="sm" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Trigger Eventing Based on Task"}>
|
||||
<IconButton onClick={()=>{onTriggerEventing();}} style={{paddingBottom: "3px"}}>
|
||||
<PlayCircleFilledTwoToneIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
{task.opsec_pre_blocked === null ? null : ( task.opsec_pre_bypassed === false ? (
|
||||
<MythicStyledTooltip title={"Submit OPSEC PreCheck Bypass Request"}>
|
||||
<IconButton onClick={()=>{setOpenOpsecDialog({open: true, view: "pre"})}} style={{paddingBottom: "3px"}}>
|
||||
<LockIcon style={{color: theme.palette.error.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
): (
|
||||
<MythicStyledTooltip title={"View OPSEC PreCheck Data"}>
|
||||
<IconButton onClick={()=>{setOpenOpsecDialog({open: true, view: "pre"})}} style={{paddingBottom: "3px"}}>
|
||||
<LockOpenIcon style={{color: theme.palette.success.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
)
|
||||
)
|
||||
}
|
||||
{task.opsec_post_blocked === null ? null : ( task.opsec_post_bypassed === false ? (
|
||||
<MythicStyledTooltip title={"Submit OPSEC PostCheck Bypass Request"}>
|
||||
<IconButton onClick={()=>{setOpenOpsecDialog({open: true, view: "post"})}} style={{paddingBottom: "3px"}}>
|
||||
<LockIcon style={{color: theme.palette.error.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
): (
|
||||
<MythicStyledTooltip title={"View OPSEC PostCheck Data"}>
|
||||
<IconButton onClick={()=>{setOpenOpsecDialog({open: true, view: "post"})}} style={{paddingBottom: "3px"}}>
|
||||
<LockOpenIcon style={{color: theme.palette.success.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
)
|
||||
)
|
||||
}
|
||||
{task.token === null ? null : (
|
||||
<MythicStyledTooltip title={"View Token Information"}>
|
||||
<IconButton onClick={()=>{setOpenTokenDialog(true)}} style={{paddingBottom: "3px"}}>
|
||||
<ConfirmationNumberIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
)}
|
||||
{task.status.toLowerCase().includes("error: container") &&
|
||||
<MythicStyledTooltip title={"Resubmit Tasking"}>
|
||||
<IconButton onClick={onReissueTask} style={{paddingBottom: "3px"}}>
|
||||
<ReplayIcon style={{color: theme.palette.warning.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
{task.status.toLowerCase().includes("error: task") &&
|
||||
<MythicStyledTooltip title={"Resubmit Task Handler"}>
|
||||
<IconButton onClick={onReissueTaskHandler} style={{paddingBottom: "3px"}}>
|
||||
<ReplayIcon style={{color: theme.palette.warning.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
</Paper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput,
|
||||
toggleOpenSearch, taskData, viewAllOutput, me,
|
||||
responseRef, style, fabStyle, viewBrowserScript}) => {
|
||||
const theme = useTheme();
|
||||
const [task, setTask] = React.useState(taskData || {});
|
||||
const eventingDataRef = React.useRef({name: "", value: 0});
|
||||
const [openEventingDialog, setOpenEventingDialog] = React.useState(false);
|
||||
const [openSpeedDial, setOpenSpeedDial] = React.useState(false);
|
||||
const [openTaskTagDialog, setOpenTaskTagDialog] = React.useState(false);
|
||||
const [openCommentDialog, setOpenCommentDialog] = React.useState(false);
|
||||
@@ -275,45 +547,60 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput
|
||||
const onReissueTaskHandler = () => {
|
||||
reissueTaskHandler({variables: {task_id: task.id}});
|
||||
}
|
||||
const onTriggerEventing = () => {
|
||||
eventingDataRef.current = {
|
||||
name: "task_id",
|
||||
value: task.id
|
||||
};
|
||||
setOpenEventingDialog(true);
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Backdrop open={openSpeedDial} onClick={()=>{setOpenSpeedDial(false);}} style={{zIndex: 2, position: "absolute"}}/>
|
||||
{openTaskTagDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="lg" open={openTaskTagDialog}
|
||||
{openTaskTagDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openTaskTagDialog}
|
||||
onClose={()=>{setOpenTaskTagDialog(false);}}
|
||||
innerDialog={<ViewEditTagsDialog me={me} target_object={"task_id"} target_object_id={task.id} onClose={()=>{setOpenTaskTagDialog(false);}} />}
|
||||
/>) : null}
|
||||
{openCommentDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="lg" open={openCommentDialog}
|
||||
/>
|
||||
}
|
||||
{openCommentDialog && <MythicDialog fullWidth={true} maxWidth="md" open={openCommentDialog}
|
||||
onClose={()=>{setOpenCommentDialog(false);}}
|
||||
innerDialog={<TaskCommentDialog task_id={task.id} onClose={()=>{setOpenCommentDialog(false);}} />}
|
||||
/>) : null
|
||||
/>
|
||||
}
|
||||
{openParametersDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="lg" open={openParametersDialog}
|
||||
{openParametersDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openParametersDialog}
|
||||
onClose={()=>{setOpenParametersDialog(false);}}
|
||||
innerDialog={<TaskViewParametersDialog task_id={task.id} onClose={()=>{setOpenParametersDialog(false);}} />}
|
||||
/>) : null
|
||||
/>
|
||||
}
|
||||
{openTokenDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="md" open={openTokenDialog}
|
||||
{openTokenDialog && <MythicDialog fullWidth={true} maxWidth="md" open={openTokenDialog}
|
||||
onClose={()=>{setOpenTokenDialog(false);}}
|
||||
innerDialog={<TaskTokenDialog token_id={task.token === undefined ? 0 : task.token.id} onClose={()=>{setOpenTokenDialog(false);}} />}
|
||||
/>) : null
|
||||
/>
|
||||
}
|
||||
{openOpsecDialog.open ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="md" open={openOpsecDialog.open}
|
||||
{openOpsecDialog.open && <MythicDialog fullWidth={true} maxWidth="lg" open={openOpsecDialog.open}
|
||||
onClose={()=>{setOpenOpsecDialog({...openOpsecDialog, open: false});}}
|
||||
innerDialog={<TaskOpsecDialog task_id={task.id} view={openOpsecDialog.view} onClose={()=>{setOpenOpsecDialog({...openOpsecDialog, open: false});}} />}
|
||||
/>) : null
|
||||
/>
|
||||
}
|
||||
|
||||
{openStdoutStderrDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="lg" open={openStdoutStderrDialog}
|
||||
{openStdoutStderrDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openStdoutStderrDialog}
|
||||
onClose={()=>{setOpenStdoutStderrDialog(false);}}
|
||||
innerDialog={<TaskViewStdoutStderrDialog task_id={task.id} onClose={()=>{setOpenStdoutStderrDialog(false);}} />}
|
||||
/>) : null
|
||||
/>
|
||||
}
|
||||
{openEventingDialog &&
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
maxWidth="xl"
|
||||
open={openEventingDialog}
|
||||
onClose={() => {setOpenEventingDialog(false);}}
|
||||
innerDialog={
|
||||
<EventTriggerContextSelectDialog onClose={() => {setOpenEventingDialog(false);}}
|
||||
triggerContext={eventingDataRef.current}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
<SpeedDial
|
||||
ariaLabel="Task Speeddial"
|
||||
icon={<SettingsTwoToneIcon fontSize={"large"} />}
|
||||
@@ -400,6 +687,13 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput
|
||||
tooltipTitle={"View Stdout/Stderr of Task"}
|
||||
onClick={()=>{setOpenStdoutStderrDialog(true);setOpenSpeedDial(false);}}
|
||||
/>
|
||||
<SpeedDialAction
|
||||
icon={<PlayCircleFilledTwoToneIcon />}
|
||||
arrow
|
||||
tooltipPlacement={"top"}
|
||||
tooltipTitle={"Trigger Eventing Based on Task"}
|
||||
onClick={()=>{onTriggerEventing();setOpenSpeedDial(false);}}
|
||||
/>
|
||||
{task.opsec_pre_blocked === null ? null : ( task.opsec_pre_bypassed === false ? (
|
||||
<SpeedDialAction
|
||||
icon={<LockIcon style={{color: theme.palette.error.main}}/>}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {Backdrop, CircularProgress} from '@mui/material';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import {b64DecodeUnicode} from './ResponseDisplay';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import {MythicDraggableDialogTitle} from "../../MythicComponents/MythicDraggableDialogTitle";
|
||||
|
||||
//if we need to get all the loaded commands for the callback and filter, use this
|
||||
const GetLoadedCommandsQuery = gql`
|
||||
@@ -256,6 +257,7 @@ query getCommandQuery($id: Int!){
|
||||
parameter_group_name
|
||||
display_name
|
||||
cli_name
|
||||
verifier_regex
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -386,10 +388,6 @@ export function TaskParametersDialog(props) {
|
||||
const removedCredential = (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 setSubmenuOpenPreventTasking = (open) => {
|
||||
setSubmenuOpenPreventTask(open);
|
||||
}
|
||||
@@ -519,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;
|
||||
}
|
||||
}
|
||||
@@ -543,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;
|
||||
}
|
||||
}
|
||||
@@ -964,7 +978,7 @@ export function TaskParametersDialog(props) {
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">{commandInfo.cmd}'s Parameters</DialogTitle>
|
||||
<MythicDraggableDialogTitle>{commandInfo.cmd}'s Parameters</MythicDraggableDialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}}>
|
||||
<CircularProgress color="inherit" />
|
||||
|
||||
@@ -27,20 +27,20 @@ import MythicStyledTableCell from '../../MythicComponents/MythicTableCell';
|
||||
import {MythicFileContext} from "../../MythicComponents/MythicFileContext";
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import {updateCredentialDeleted} from "../Search/CredentialTable";
|
||||
import CloudUploadTwoToneIcon from '@mui/icons-material/CloudUploadTwoTone';
|
||||
|
||||
export const getDynamicQueryParams = gql`
|
||||
export const getDynamicQueryParamsString = `
|
||||
mutation getDynamicParamsMutation($callback: Int!, $command: String!, $payload_type: String!, $parameter_name: String!, $other_parameters: jsonb){
|
||||
dynamic_query_function(callback: $callback, command: $command, payload_type: $payload_type, parameter_name: $parameter_name, other_parameters: $other_parameters){
|
||||
status
|
||||
error
|
||||
choices
|
||||
parameter_name
|
||||
complex_choices {
|
||||
value
|
||||
display_value
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
export const getDynamicQueryParams = gql`
|
||||
${getDynamicQueryParamsString}
|
||||
`;
|
||||
const parseTypedArrayMutation = gql`
|
||||
mutation parseTypedArrayMutation($callback: Int!, $command: String!, $payload_type: String!, $parameter_name: String!, $input_array: [String!]!){
|
||||
@@ -85,6 +85,17 @@ 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();
|
||||
@@ -117,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];
|
||||
@@ -198,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;
|
||||
@@ -283,6 +297,8 @@ export function TaskParametersDialogRow(props){
|
||||
}
|
||||
});
|
||||
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});
|
||||
@@ -296,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});
|
||||
@@ -364,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);
|
||||
}
|
||||
@@ -372,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;
|
||||
}
|
||||
setAgentConnectPayload(payloadNum);
|
||||
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);
|
||||
onChangeAgentConnect(agentConnectHost, payloadNum, 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 === ""){
|
||||
@@ -441,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}
|
||||
@@ -509,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);
|
||||
@@ -563,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);
|
||||
}
|
||||
@@ -692,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"}
|
||||
/>
|
||||
@@ -871,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":
|
||||
@@ -956,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 />}
|
||||
>
|
||||
@@ -981,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>
|
||||
@@ -993,7 +1033,7 @@ export function TaskParametersDialogRow(props){
|
||||
<MythicStyledTableCell>
|
||||
<FormControl style={{width: "100%"}}>
|
||||
<Select
|
||||
value={agentConnectHost}
|
||||
value={agentConnectHostOptions.length > 0 ? agentConnectHost : ''}
|
||||
onChange={onChangeAgentConnectHost}
|
||||
input={<Input />}
|
||||
>
|
||||
@@ -1007,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 />}
|
||||
>
|
||||
@@ -1035,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>
|
||||
|
||||
@@ -1051,7 +1092,7 @@ export function TaskParametersDialogRow(props){
|
||||
<MythicStyledTableCell>
|
||||
<FormControl style={{width: "100%"}}>
|
||||
<Select
|
||||
value={agentConnectC2Profile}
|
||||
value={agentConnectC2ProfileOptions.length > 0 ? agentConnectC2Profile : ''}
|
||||
onChange={onChangeAgentConnectC2Profile}
|
||||
input={<Input />}
|
||||
>
|
||||
@@ -1147,15 +1188,16 @@ export function TaskParametersDialogRow(props){
|
||||
return (
|
||||
<TableRow key={"buildparam" + props.id}>
|
||||
<MythicStyledTableCell >
|
||||
<Typography style={{fontWeight: "600"}} >
|
||||
<Typography style={{fontWeight: "600", wordBreak: "break-all"}} >
|
||||
{props.display_name}
|
||||
</Typography>
|
||||
<Typography variant={"body2"} style={{fontSize: theme.typography.pxToRem(15)}}>
|
||||
<Typography variant={"body2"} style={{
|
||||
fontSize: theme.typography.pxToRem(14),
|
||||
wordBreak: "break-all"
|
||||
}}>
|
||||
{props.description}
|
||||
</Typography>
|
||||
{props.required ? (
|
||||
<Typography component="div" color={"warning"}>Required</Typography>
|
||||
) : null }
|
||||
{props.required && <Typography component="div" color={"warning"}>Required</Typography>}
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
{getParameterObject()}
|
||||
@@ -1164,3 +1206,115 @@ export function TaskParametersDialogRow(props){
|
||||
)
|
||||
}
|
||||
|
||||
export const DragAndDropFileUpload = ({value, values, multiple, onChange}) => {
|
||||
const theme = useTheme();
|
||||
const inputRef = React.useRef(null);
|
||||
const [files, setFiles] = React.useState(values ? values : []);
|
||||
const [file, setFile] = React.useState(value ? value : {name: ""});
|
||||
const [isDragging, setIsDragging] = React.useState(false);
|
||||
|
||||
React.useEffect( () => {
|
||||
if(value){
|
||||
setFile(value);
|
||||
}
|
||||
if(values){
|
||||
setFiles(values);
|
||||
}
|
||||
}, [value, values]);
|
||||
const handleDragEnter = () => setIsDragging(true);
|
||||
const handleDragLeave = (event) => {
|
||||
if (event.currentTarget.contains(event.relatedTarget)) {
|
||||
return; // Don't treat as a real leave event
|
||||
}
|
||||
setIsDragging(false);
|
||||
}
|
||||
const handleDrop = (e) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
const droppedFiles = Array.from(e.dataTransfer.files);
|
||||
if(multiple){
|
||||
setFiles(droppedFiles);
|
||||
onChange(droppedFiles)
|
||||
} else {
|
||||
setFile(droppedFiles[0]);
|
||||
onChange(droppedFiles[0]);
|
||||
}
|
||||
};
|
||||
const handleDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
const onFileChange = (evt) => {
|
||||
setFile({name: evt.target.files[0].name});
|
||||
onChange(evt.target.files[0]);
|
||||
}
|
||||
const onFileMultChange = (evt) => {
|
||||
setFiles([...evt.target.files]);
|
||||
onChange(evt.target.files)
|
||||
}
|
||||
const onClick = (e) => {
|
||||
inputRef.current.click();
|
||||
}
|
||||
return (
|
||||
<div
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onClick={onClick}
|
||||
style={{
|
||||
border: isDragging ? `4px dashed ${theme.palette.success.main}` : `4px dashed ${theme.palette.primary.main}`,
|
||||
padding: "20px",
|
||||
textAlign: "center",
|
||||
borderRadius: "10px",
|
||||
cursor: "pointer"
|
||||
//backgroundColor: isDragging ? "#f0f8ff" : "#fff",
|
||||
}}
|
||||
>
|
||||
<input ref={inputRef} onChange={multiple ? onFileMultChange : onFileChange} type="file" hidden multiple={multiple} />
|
||||
<div style={{display: "flex", flexDirection: "column", alignItems: "center"}}>
|
||||
{!multiple && file.name !== "" &&
|
||||
<>
|
||||
<CloudUploadTwoToneIcon fontSize={"large"} color={"success"} />
|
||||
<Typography>
|
||||
Selected:
|
||||
</Typography>
|
||||
<Typography>
|
||||
{!file.legacy && file.name}
|
||||
{file.legacy &&
|
||||
<MythicFileContext agent_file_id={file.name}
|
||||
extraStyles={{ position: "relative", marginLeft: "5px", marginRight: "5px"}} />
|
||||
}
|
||||
</Typography>
|
||||
</>
|
||||
|
||||
}
|
||||
{multiple && files.length > 0 &&
|
||||
<>
|
||||
<CloudUploadTwoToneIcon fontSize={"large"} color={"success"} />
|
||||
<Typography>
|
||||
Selected:
|
||||
</Typography>
|
||||
{files.map( (f, i) => (
|
||||
<div key={i}>
|
||||
{typeof f === "string" && <MythicFileContext agent_file_id={f} />}
|
||||
{typeof f !== "string" && (f.name)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
|
||||
}
|
||||
{file.name === "" && files.length === 0 &&
|
||||
<>
|
||||
<CloudUploadTwoToneIcon fontSize={"large"} color={"success"} />
|
||||
<Typography>
|
||||
Drag and drop files here
|
||||
</Typography>
|
||||
<Typography>
|
||||
Click to open dialog
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {MythicDraggableDialogTitle} from "../../MythicComponents/MythicDraggableDialogTitle";
|
||||
|
||||
export const allTokenDataFragment = gql`
|
||||
fragment allTokenData on token {
|
||||
@@ -89,7 +89,7 @@ export function TaskTokenDialog(props) {
|
||||
});
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Token Information</DialogTitle>
|
||||
<MythicDraggableDialogTitle >Token Information</MythicDraggableDialogTitle>
|
||||
<TableContainer className="mythicElement">
|
||||
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
|
||||
<TableHead>
|
||||
|
||||
@@ -68,13 +68,14 @@ export function TaskViewParametersDialog(props) {
|
||||
return <div>Error!</div>;
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicModifyStringDialog title={`View Task Parameters And Timestamps`}
|
||||
onClose={props.onClose}
|
||||
maxRows={40}
|
||||
wrap={true}
|
||||
value={comment} />
|
||||
</React.Fragment>
|
||||
<>
|
||||
<MythicModifyStringDialog title={`View Task Parameters And Timestamps`}
|
||||
onClose={props.onClose}
|
||||
maxRows={40}
|
||||
wrap={true}
|
||||
value={comment} />
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ query getParametersQuery ($task_id: Int!) {
|
||||
|
||||
export function TaskViewStdoutStderrDialog(props) {
|
||||
const [comment, setComment] = useState("");
|
||||
const theme = useTheme();
|
||||
const { loading, error } = useQuery(getParametersQuery, {
|
||||
variables: {task_id: props.task_id},
|
||||
onCompleted: data => {
|
||||
|
||||
@@ -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");
|
||||
@@ -61,34 +73,39 @@ export function CreatePayload(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>
|
||||
'& .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>
|
||||
</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>
|
||||
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
@@ -21,24 +20,127 @@ 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]}
|
||||
@@ -81,20 +183,24 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
|
||||
const submitDictChange = (list) => {
|
||||
onChange(name, list, false);
|
||||
};
|
||||
const onFileChange = (evt) => {
|
||||
setFileValue({name: evt.target.files[0].name});
|
||||
onChange(name, evt.target.files[0]);
|
||||
const onFileChange = (newFile) => {
|
||||
setFileValue({name: newFile.name});
|
||||
onChange(name, newFile);
|
||||
}
|
||||
const onFileMultChange = (evt) => {
|
||||
setFileMultValue([...evt.target.files]);
|
||||
onChange(name, [...evt.target.files]);
|
||||
const onFileMultChange = (newFiles) => {
|
||||
setFileMultValue([...newFiles]);
|
||||
onChange(name, [...newFiles]);
|
||||
}
|
||||
useEffect( () => {
|
||||
if(parameter_type === "ChooseOne" || parameter_type === "ChooseOneCustom"){
|
||||
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("");
|
||||
@@ -105,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 !== ""});
|
||||
@@ -165,7 +271,19 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
|
||||
}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 === ""){
|
||||
@@ -255,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);
|
||||
@@ -357,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
|
||||
@@ -414,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>
|
||||
))
|
||||
}
|
||||
@@ -424,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 (
|
||||
@@ -458,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>
|
||||
@@ -562,7 +702,7 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
|
||||
return (
|
||||
<Switch
|
||||
color={"info"}
|
||||
checked={Boolean(value)}
|
||||
checked={isTrue(value)}
|
||||
onChange={toggleSwitchValue}
|
||||
inputProps={{ 'aria-label': 'info checkbox' }}
|
||||
/>
|
||||
|
||||
@@ -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 ) => {
|
||||
@@ -107,23 +111,23 @@ export function Step3SelectCommands(props){
|
||||
}
|
||||
}
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -134,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?"}
|
||||
@@ -145,139 +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) => () => {
|
||||
if(value.disabled){
|
||||
return;
|
||||
}
|
||||
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);
|
||||
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) => (
|
||||
<div style={{width:"100%", height: "calc(40vh)", display: "flex", flexDirection: "column", overflowY: "auto"}} >
|
||||
<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>
|
||||
);
|
||||
@@ -287,24 +428,16 @@ function CommandTransferSelect(props) {
|
||||
</div>
|
||||
</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"
|
||||
>
|
||||
>>
|
||||
@@ -314,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"
|
||||
>
|
||||
>
|
||||
@@ -324,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"
|
||||
>
|
||||
<
|
||||
@@ -334,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,122 +232,293 @@ 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"}}>
|
||||
@@ -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"} marginTop={"0px"}
|
||||
onChange={handleSearchValueChange} onEnter={submitSearch} InputProps={{
|
||||
endAdornment:
|
||||
<React.Fragment>
|
||||
<Tooltip title="Search">
|
||||
<IconButton onClick={submitSearch} size="large"><SearchIcon style={{color: theme.palette.info.main}}/></IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Resolve Viewable Errors">
|
||||
<IconButton onClick={props.resolveViewableErrors} size="large"><AutoFixHighIcon style={{color: theme.palette.success.main}}/></IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Resolve All Errors">
|
||||
<IconButton onClick={props.resolveAllErrors} size="large"><HealingIcon style={{color: theme.palette.success.main}}/></IconButton>
|
||||
</Tooltip>
|
||||
</React.Fragment>,
|
||||
style: {padding: 0}
|
||||
}}/>
|
||||
</Grid>
|
||||
<Grid style={{paddingTop: 0, paddingLeft: 0}} size={2}>
|
||||
<Select
|
||||
style={{width: "100%"}}
|
||||
value={level}
|
||||
onChange={handleLevelValueChange}
|
||||
>
|
||||
{
|
||||
levelOptions.map((opt, i) => (
|
||||
<MenuItem key={"levelFilter" + opt} value={opt}>{opt}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
</Grid>
|
||||
<MythicPageBody>
|
||||
<Grid container spacing={1} style={{ marginTop: "0px"}}>
|
||||
<Grid style={{paddingTop: 0}} size={10}>
|
||||
<MythicTextField placeholder="Search..." value={search} marginBottom={"0px"} marginTop={"0px"}
|
||||
onChange={handleSearchValueChange} onEnter={submitSearch} InputProps={{
|
||||
endAdornment:
|
||||
<React.Fragment>
|
||||
<Tooltip title="Search">
|
||||
<IconButton onClick={submitSearch} size="large"><SearchIcon style={{color: theme.palette.info.main}}/></IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Resolve Viewable Errors">
|
||||
<IconButton onClick={props.resolveViewableErrors} size="large"><AutoFixHighIcon style={{color: theme.palette.success.main}}/></IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Resolve All Errors">
|
||||
<IconButton onClick={props.resolveAllErrors} size="large"><HealingIcon style={{color: theme.palette.success.main}}/></IconButton>
|
||||
</Tooltip>
|
||||
</React.Fragment>,
|
||||
style: {padding: 0}
|
||||
}}/>
|
||||
</Grid>
|
||||
</Paper>
|
||||
<Grid style={{paddingTop: 0, paddingLeft: 0}} size={2}>
|
||||
<Select
|
||||
style={{width: "100%"}}
|
||||
value={level}
|
||||
onChange={handleLevelValueChange}
|
||||
>
|
||||
{
|
||||
levelOptions.map((opt, i) => (
|
||||
<MenuItem key={"levelFilter" + opt} value={opt}>{opt}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<div style={{display: "flex", flexDirection: "column", overflowY: "auto", flexGrow: 1, overflowX: "Hidden"}}>
|
||||
<EventList
|
||||
onUpdateResolution={props.onUpdateResolution}
|
||||
@@ -99,6 +99,6 @@ export function EventFeedTable(props){
|
||||
siblingCount={1} onChange={props.onChangePage} showFirstButton={true} showLastButton={true} style={{padding: "20px"}}/>
|
||||
<Typography style={{paddingLeft: "10px"}}>Total Results: {props.pageData.totalCount}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</MythicPageBody>
|
||||
);
|
||||
}
|
||||
@@ -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,6 +175,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,
|
||||
@@ -254,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,
|
||||
@@ -270,6 +300,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, "previous_checkin", "checkin_difference"],
|
||||
@@ -594,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>
|
||||
@@ -646,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>
|
||||
@@ -1033,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>
|
||||
@@ -1224,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>
|
||||
@@ -1348,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>
|
||||
@@ -1492,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>
|
||||
@@ -1575,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>
|
||||
@@ -1636,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>
|
||||
@@ -1698,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>
|
||||
@@ -1789,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>
|
||||
@@ -1935,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>
|
||||
@@ -2097,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>
|
||||
@@ -2297,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>
|
||||
@@ -2501,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>
|
||||
);
|
||||
}
|
||||
@@ -126,6 +126,8 @@ export function EventTriggerKeywordDialog({onClose, selectedEventGroup}) {
|
||||
<TableCell style={{width: "50%"}}>Key</TableCell>
|
||||
<TableCell>Value</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{dictionaryData.map( (e, index) => (
|
||||
<TableRow key={"dictionarydata" + index}>
|
||||
<MythicStyledTableCell style={{display: "flex"}} >
|
||||
@@ -154,8 +156,6 @@ export function EventTriggerKeywordDialog({onClose, selectedEventGroup}) {
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell>
|
||||
<Button onClick={addRow} color={"success"}>
|
||||
|
||||
@@ -21,6 +21,8 @@ import {TestEventGroupFileDialog} from "./CreateEventWorkflowDialog";
|
||||
import AddCircleIcon from '@mui/icons-material/AddCircle';
|
||||
import Split from 'react-split';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {CreateEventingStepper} from "./CreateEventingStepper";
|
||||
import CategoryIcon from '@mui/icons-material/Category';
|
||||
|
||||
const get_eventgroups = gql`
|
||||
query GetEventGroups {
|
||||
@@ -165,6 +167,7 @@ steps:
|
||||
export function Eventing({me}){
|
||||
const theme = useTheme();
|
||||
const [openTestModal, setOpenTestModal] = React.useState(false);
|
||||
const [openCreateEventingStepper, setOpenCreateEventingStepper] = React.useState(false);
|
||||
const [eventgroups, setEventgroups] = React.useState([]);
|
||||
const [showDeleted, setShowDeleted] = React.useState(false);
|
||||
const [selectedEventGroup, setSelectedEventGroup] = React.useState({id: 0});
|
||||
@@ -233,6 +236,11 @@ export function Eventing({me}){
|
||||
}
|
||||
evt.target.value = null;
|
||||
}
|
||||
const onCloseStepper = (e, success) => {
|
||||
if(success === true){
|
||||
setOpenCreateEventingStepper(false);
|
||||
}
|
||||
}
|
||||
React.useEffect( () => {
|
||||
if( !foundQueryEvent.current ){
|
||||
let queryParams = new URLSearchParams(window.location.search);
|
||||
@@ -267,9 +275,18 @@ export function Eventing({me}){
|
||||
<Button size={"small"} color={"success"}
|
||||
style={{display: "inline-flex", marginRight: "10px", marginLeft: "10px", marginTop: "5px"}}
|
||||
onClick={()=>setOpenTestModal(true)}
|
||||
startIcon={<AddCircleIcon />}
|
||||
startIcon={<AddCircleIcon style={{backgroundColor: "white", borderRadius: "10px"}} />}
|
||||
>
|
||||
Create New
|
||||
Text
|
||||
</Button>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Create Workflow with GUI Wizard"} tooltipStyle={{}} >
|
||||
<Button size={"small"} color={"success"}
|
||||
style={{display: "inline-flex", marginRight: "10px", marginLeft: "10px", marginTop: "5px"}}
|
||||
onClick={()=>setOpenCreateEventingStepper(true)}
|
||||
startIcon={<CategoryIcon />}
|
||||
>
|
||||
Wizard
|
||||
</Button>
|
||||
</MythicStyledTooltip>
|
||||
</Paper>
|
||||
@@ -286,9 +303,17 @@ export function Eventing({me}){
|
||||
}}/>}
|
||||
/>
|
||||
}
|
||||
{openCreateEventingStepper &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openCreateEventingStepper}
|
||||
onClose={() => setOpenCreateEventingStepper(false)}
|
||||
innerDialog={<CreateEventingStepper onClose={onCloseStepper} />}
|
||||
/>
|
||||
}
|
||||
<ListItem onClick={() => setSelectedEventGroup({id: 0})}
|
||||
className={selectedEventGroup.id === 0 ? "selectedCallback": ""}
|
||||
style={selectedEventGroup.id === 0 ?
|
||||
{paddingTop: 0, paddingBottom: 0, borderLeft: `5px solid ${theme.palette.info.main}`} :
|
||||
{paddingTop: 0, paddingBottom: 0, borderLeft: `5px solid ${theme.palette.info.main}`,
|
||||
borderRight: `5px solid ${theme.palette.info.main}`} :
|
||||
{paddingTop: 0, paddingBottom: 0}}>
|
||||
<ListItemText primary={"View All Instances"} />
|
||||
</ListItem>
|
||||
@@ -312,8 +337,11 @@ export function Eventing({me}){
|
||||
{eventgroups.map( (e, i) => (
|
||||
(showDeleted || !e.deleted) &&
|
||||
<ListItem key={e.id + e.name} onClick={() => setSelectedEventGroup(e)}
|
||||
className={selectedEventGroup.id === e.id ? "selectedCallback": ""}
|
||||
style={selectedEventGroup.id === e.id ?
|
||||
{paddingTop: 0, paddingBottom: 0, borderLeft: `5px solid ${theme.palette.info.main}`} :
|
||||
{paddingTop: 0, paddingBottom: 0,
|
||||
borderLeft: `5px solid ${theme.palette.info.main}`, borderRight: `5px solid ${theme.palette.info.main}`,
|
||||
} :
|
||||
{paddingTop: 0, paddingBottom: 0}}>
|
||||
{!e.active &&
|
||||
<NotificationsOffTwoToneIcon color={"warning"} />
|
||||
@@ -330,7 +358,7 @@ export function Eventing({me}){
|
||||
</div>
|
||||
<div className="bg-gray-light" style={{display: "inline-flex"}}>
|
||||
<div style={{width: "100%", height: "100%"}}>
|
||||
<EventGroupTable selectedEventGroup={selectedEventGroup} me={me} />
|
||||
<EventGroupTable selectedEventGroup={selectedEventGroup} me={me} showInstances={true} showGraph={true} />
|
||||
</div>
|
||||
</div>
|
||||
</Split>
|
||||
|
||||
@@ -59,8 +59,8 @@ export function ExpandedCallbackSideDetailsTable(props){
|
||||
<Table size="small" style={{tableLayout: "fixed"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{width: "12rem"}}></TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell style={{width: "12rem"}}>Callback Info</TableCell>
|
||||
<TableCell>Value</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody style={{whiteSpace: "pre-wrap", wordBreak: "break-all"}}>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
export const Hasura = () => {
|
||||
return (
|
||||
<iframe height={"100%"} width={"100%"} style={{border:"none"}}
|
||||
src={"/console"} title={"Hasura Console"} />
|
||||
)
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useQuery, gql} from '@apollo/client';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip';
|
||||
|
||||
const GetC2ProfilesAndPayloadTypes = gql`
|
||||
query GetC2AndPayloadType {
|
||||
c2profile(where: {deleted: {_eq: false}}) {
|
||||
name
|
||||
id
|
||||
container_running
|
||||
}
|
||||
payloadtype(where: {deleted: {_eq: false}, agent_type: {_eq: "agent"}}) {
|
||||
name
|
||||
wrapper
|
||||
id
|
||||
container_running
|
||||
payloadtypec2profiles {
|
||||
c2profile {
|
||||
name
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
wrappers: payloadtype(where: {deleted: {_eq: false}, agent_type: {_eq: "agent"}, wrapper: {_eq: true}}) {
|
||||
name
|
||||
wrapper
|
||||
id
|
||||
container_running
|
||||
wrap_these_payload_types {
|
||||
wrapped {
|
||||
wrapper
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function AgentC2Overview(props){
|
||||
const theme = useTheme();
|
||||
const [c2Profiles, setC2Profiles] = React.useState([]);
|
||||
const [payloadTypeRows, setPayloadTypeRows] = React.useState([]);
|
||||
const [payloadTypeRowsNoWrappers, setPayloadTypeRowsNoWrappers] = React.useState([]);
|
||||
const [wrappers, setWrappers] = React.useState([]);
|
||||
const { loading } = useQuery(GetC2ProfilesAndPayloadTypes, {fetchPolicy: "network-only",
|
||||
onCompleted: (data) => {
|
||||
const c2Headers = data.c2profile.map( (c2) => c2.name);
|
||||
const payloadRows = data.payloadtype.map( (payload) => {
|
||||
const payloadc2 = payload.payloadtypec2profiles.map( (c2) => {
|
||||
return c2.c2profile.name;
|
||||
})
|
||||
return {name: payload.name, payloadtypec2profiles: payloadc2, wrapper: payload.wrapper };
|
||||
});
|
||||
|
||||
c2Headers.sort();
|
||||
payloadRows.sort( (a,b) => a.name < b.name ? -1 : 1);
|
||||
const payloadTypeNoWrappers = payloadRows.filter( p => !p.wrapper);
|
||||
const wrapperRows = data.wrappers.map( (payload) => {
|
||||
const wrapped = payload.wrap_these_payload_types.map( (w) => {
|
||||
return w.wrapped.name;
|
||||
});
|
||||
return {name: payload.name, wrapped}
|
||||
});
|
||||
wrapperRows.sort( (a,b) => a.name < b.name ? -1 : 1);
|
||||
setWrappers(wrapperRows);
|
||||
setC2Profiles(c2Headers);
|
||||
setPayloadTypeRows(payloadRows);
|
||||
setPayloadTypeRowsNoWrappers(payloadTypeNoWrappers);
|
||||
},
|
||||
onError: (data) => {
|
||||
|
||||
}
|
||||
});
|
||||
if (loading) {
|
||||
return <LinearProgress />;
|
||||
}
|
||||
return (
|
||||
<div style={{ marginTop: "10px", marginRight: "5px"}}>
|
||||
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main, marginBottom: "5px", marginTop: "10px", marginRight: "5px"}} variant={"elevation"}>
|
||||
<Typography variant="h3" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
|
||||
Agent / C2 Overview
|
||||
</Typography>
|
||||
</Paper>
|
||||
<TableContainer component={Paper} className="mythicElement">
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow hover>
|
||||
<TableCell></TableCell>
|
||||
{c2Profiles.map( (c2) => (
|
||||
<TableCell key={c2}>{c2}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{payloadTypeRowsNoWrappers.map( (payload) => (
|
||||
<TableRow key={payload.name} hover>
|
||||
<TableCell>{payload.name}</TableCell>
|
||||
{c2Profiles.map( (c2) => (
|
||||
<TableCell key={'payload' + c2}>
|
||||
{payload.payloadtypec2profiles.includes(c2) ?
|
||||
<MythicStyledTooltip title={payload.name + " supports " + c2}>
|
||||
<CheckCircleIcon color="success"/>
|
||||
</MythicStyledTooltip>
|
||||
: ""}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{wrappers.length > 0 &&
|
||||
<TableContainer component={Paper} className="mythicElement" style={{marginTop: "10px"}}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow hover>
|
||||
<TableCell></TableCell>
|
||||
{payloadTypeRows.map( (pt) => (
|
||||
<TableCell key={'wrapped' + pt.name}>{pt.name}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{wrappers.map( (payload) => (
|
||||
<TableRow key={'wrapper' + payload.name} hover>
|
||||
<TableCell>{payload.name}</TableCell>
|
||||
{payloadTypeRows.map( (wr) => (
|
||||
<TableCell key={'payload' + wr.name}>
|
||||
{payload.wrapped.includes(wr.name) ?
|
||||
<MythicStyledTooltip title={payload.name + " wraps " + wr.name}>
|
||||
<CheckCircleIcon color="success"/>
|
||||
</MythicStyledTooltip>
|
||||
: ""}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -57,6 +57,7 @@ import {copyStringToClipboard} from "../../utilities/Clipboard";
|
||||
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
|
||||
import {faCopy} from '@fortawesome/free-solid-svg-icons';
|
||||
import MythicTextField from "../../MythicComponents/MythicTextField";
|
||||
import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
|
||||
|
||||
const LeadDashboardQuery = gql`
|
||||
${taskingDataFragment}
|
||||
@@ -1521,13 +1522,7 @@ const Top10RecentScreenshotsDashboardElement = ({me, data, editing, removeElemen
|
||||
<>
|
||||
<TableDataCard title={"Recent Screenshots"}
|
||||
tableHead={
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicTableCell></MythicTableCell>
|
||||
<MythicTableCell></MythicTableCell>
|
||||
<MythicTableCell></MythicTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
null
|
||||
}
|
||||
tableBody={
|
||||
<TableBody>
|
||||
@@ -1594,11 +1589,7 @@ const Top10RecentTasksDashboardElement = ({me, data, editing, removeElement}) =>
|
||||
<>
|
||||
<TableDataCard title={"Recent Tasking"}
|
||||
tableHead={
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicTableCell></MythicTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
null
|
||||
}
|
||||
tableBody={
|
||||
<TableBody>
|
||||
@@ -1714,20 +1705,20 @@ const OperatorDashboard = ({me, setLoading, loading}) => {
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div style={{display: "flex", marginLeft: "5px", marginBottom: "5px", marginTop: "5px"}}>
|
||||
<div style={{display: "flex", marginLeft: "0.5rem", marginBottom: "0.5rem",}}>
|
||||
<ActiveCallbacksDashboardElement me={me} data={analysisData}/>
|
||||
<Top10RecentPayloadsDashboardElement me={me} data={analysisData}/>
|
||||
<Top10RecentWorkflowsDashboardElement me={me} data={analysisData}/>
|
||||
</div>
|
||||
<div style={{display: "flex", marginLeft: "5px", marginBottom: "5px"}}>
|
||||
<div style={{display: "flex", marginLeft: "0.5rem", marginBottom: "0.5rem"}}>
|
||||
<ProxyUsageDashboardElement me={me} data={analysisData}/>
|
||||
<Top10RecentFileDownloadsDashboardElement me={me} data={analysisData}/>
|
||||
<Top10RecentCredentialsDashboardElement me={me} data={analysisData}/>
|
||||
</div>
|
||||
<div style={{display: "flex", marginLeft: "5px", marginBottom: "5px"}}>
|
||||
<div style={{display: "flex", marginLeft: "0.5rem", marginBottom: "0.5rem"}}>
|
||||
<Top10RecentTasksDashboardElement me={me} data={analysisData}/>
|
||||
</div>
|
||||
<div style={{display: "flex", marginLeft: "5px", marginBottom: "5px"}}>
|
||||
<div style={{display: "flex", marginLeft: "0.5rem", marginBottom: "0.5rem"}}>
|
||||
<Top10RecentScreenshotsDashboardElement me={me} data={analysisData}/>
|
||||
<MyOperationsDashboardElement me={me} data={analysisData} reloadDashboard={reloadDashboard}/>
|
||||
</div>
|
||||
@@ -1754,16 +1745,16 @@ const LeadDashboard = ({me, setLoading, loading}) => {
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<div style={{display: "flex", marginLeft: "5px", marginBottom: "5px", marginTop: "5px"}}>
|
||||
<div style={{display: "flex", marginLeft: "0.5rem", marginBottom: "0.5rem",}}>
|
||||
<ActiveCallbacksDashboardElement me={me} data={analysisData} />
|
||||
<Top10CommandStatsDashboardElement me={me} data={analysisData} />
|
||||
<Top10UserContextsDashboardElement me={me} data={analysisData} />
|
||||
<Top10HostContextsDashboardElement me={me} data={analysisData} />
|
||||
</div>
|
||||
<div style={{display: "flex", marginLeft: "5px", marginBottom: "5px"}}>
|
||||
<div style={{display: "flex", marginLeft: "0.5rem", marginBottom: "0.5rem"}}>
|
||||
<ActivityPerDayDashboardElement me={me} data={analysisData} />
|
||||
</div>
|
||||
<div style={{display: "flex", marginLeft: "5px"}}>
|
||||
<div style={{display: "flex", marginLeft: "0.5rem"}}>
|
||||
<TaskStatusDashboardElement me={me} data={analysisData} />
|
||||
<OperatorActivityDashboardElement me={me} data={analysisData} />
|
||||
<Top10ArtifactsDashboardElement me={me} data={analysisData} />
|
||||
@@ -1932,7 +1923,7 @@ const CustomDashboard = ({me, setLoading, loading, editing}) => {
|
||||
<div style={{height: "100%", overflowY: "auto"}}>
|
||||
{dashboards.map((d, i) => (
|
||||
<div key={"dashboardRow" + i}
|
||||
style={{display: "flex", marginLeft: "5px", marginTop: "5px", borderRadius: "4px", }}>
|
||||
style={{display: "flex", marginLeft: "0.5rem", marginBottom: "0.5rem", borderRadius: "5px", }}>
|
||||
{editing &&
|
||||
<div style={{display: "flex", flexDirection: "column", width: "40px", justifyContent: "center"}}>
|
||||
<MythicStyledTooltip title={"Add New Chart To This Row"}>
|
||||
@@ -1976,7 +1967,6 @@ const CustomDashboard = ({me, setLoading, loading, editing}) => {
|
||||
}
|
||||
|
||||
export function CallbacksCard({me}) {
|
||||
const theme = useTheme();
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [editing, setEditing] = React.useState(false);
|
||||
const initialDashboardView = GetMythicSetting({
|
||||
@@ -1992,24 +1982,20 @@ export function CallbacksCard({me}) {
|
||||
setLoading(true);
|
||||
}
|
||||
return (
|
||||
<div style={{width: "100%", height: "100%", display: "flex", flexDirection: "column", position: "relative"}}>
|
||||
<Paper elevation={5} variant={"elevation"} style={{
|
||||
backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,
|
||||
display: "flex", alignItems: "center", justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5" style={{textAlign: "left", display: "inline-block", marginLeft: "10px"}}>
|
||||
<>
|
||||
<MythicPageHeader title={
|
||||
<>
|
||||
Welcome <b>{me.user.username}</b>
|
||||
{me.user.current_operation_id === 0 ? null : (
|
||||
<>
|
||||
{" to"} <b>{me.user.current_operation}</b>'s Dashboard
|
||||
{" to"} <b>{me.user.current_operation}</b>'s Dashboard
|
||||
</>
|
||||
)}
|
||||
|
||||
</Typography>
|
||||
<span>
|
||||
</>
|
||||
}>
|
||||
<div style={{display: "flex", alignItems: "center"}}>
|
||||
<TextField
|
||||
style={{color: "white", marginTop: "5px", width: "10rem"}}
|
||||
style={{color: "white", width: "10rem", margin: "0.5rem"}}
|
||||
InputProps={{style: {color: "white"}}}
|
||||
size={"small"}
|
||||
select={true}
|
||||
@@ -2022,8 +2008,8 @@ export function CallbacksCard({me}) {
|
||||
<MenuItem key={"searchopt" + opt} value={opt}>{opt}</MenuItem>
|
||||
))
|
||||
}
|
||||
</TextField>
|
||||
<MythicStyledTooltip title={"Analyze Operation Data Again"}>
|
||||
</TextField>
|
||||
<MythicStyledTooltip title={"Analyze Operation Data Again"} tooltipStyle={{display: "inline-block"}}>
|
||||
<IconButton style={{color: "white"}} onClick={() => setLoading(true)}>
|
||||
<ReplayIcon />
|
||||
</IconButton>
|
||||
@@ -2035,8 +2021,8 @@ export function CallbacksCard({me}) {
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
</span>
|
||||
</Paper>
|
||||
</div>
|
||||
</MythicPageHeader>
|
||||
<div style={{width: "100%", height: "100%", display: "flex", flexDirection: "column", position: "relative", overflowY: "auto"}}>
|
||||
{loading &&
|
||||
<div style={{
|
||||
@@ -2070,7 +2056,7 @@ export function CallbacksCard({me}) {
|
||||
me={me} loading={loading} setLoading={setLoading} editing={editing}
|
||||
/>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import Paper from "@mui/material/Paper";
|
||||
|
||||
const normalColors = [
|
||||
'#09bdff',
|
||||
@@ -45,16 +46,17 @@ export const PieChartCard = ({
|
||||
const toggleLegend = () => {
|
||||
setShowLegend(!showLegend);
|
||||
}
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<div style={{
|
||||
marginRight: "5px",
|
||||
<Paper variant={"elevation"} elevation={1} style={{
|
||||
marginRight: "0.5rem",
|
||||
width: width,
|
||||
height: "100%",
|
||||
border: "1px solid gray",
|
||||
borderRadius: "4px",
|
||||
border: `1px solid ${theme.borderColor}`,
|
||||
borderRadius: "5px",
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<h3 style={{marginTop: 0, marginLeft: "5px", marginBottom: 0, paddingBottom: 0,}}>
|
||||
<h3 style={{marginTop: 0, marginLeft: "0.5rem", marginBottom: 0, paddingBottom: 0,}}>
|
||||
{editing &&
|
||||
<span>
|
||||
<MythicStyledTooltip title={"Remove element"}>
|
||||
@@ -120,7 +122,7 @@ export const PieChartCard = ({
|
||||
}}>
|
||||
{innerElement}
|
||||
</PieChart>
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
function GaugePointer() {
|
||||
@@ -151,8 +153,7 @@ export const GaugeCard = ({data, width = "100%", additionalStyles, innerElement,
|
||||
right: 10,
|
||||
top: 10,
|
||||
bottom: 10,
|
||||
}, colors = normalColors, onClick, title = "", editing, removeElement, customizeElement
|
||||
}) => {
|
||||
}, colors = normalColors, onClick, title = "", editing, removeElement, customizeElement }) => {
|
||||
const theme = useTheme();
|
||||
const getFillColor = () => {
|
||||
if(data['total'] === 0){return theme.palette.text.disabled}
|
||||
@@ -166,15 +167,15 @@ export const GaugeCard = ({data, width = "100%", additionalStyles, innerElement,
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div style={{
|
||||
marginRight: "5px",
|
||||
<Paper variant={"elevation"} elevation={1} style={{
|
||||
marginRight: "0.5rem",
|
||||
width: width,
|
||||
height: "100%",
|
||||
border: "1px solid gray",
|
||||
border: `1px solid ${theme.borderColor}`,
|
||||
borderRadius: "5px",
|
||||
overflow: "hidden",
|
||||
borderRadius: "4px",
|
||||
}}>
|
||||
<h3 style={{marginTop: 0, marginLeft: "5px", marginBottom: 0, paddingBottom: 0,}}>
|
||||
<h3 style={{marginTop: 0, marginLeft: "0.5rem", marginBottom: 0, paddingBottom: 0,}}>
|
||||
{editing &&
|
||||
<span>
|
||||
<MythicStyledTooltip title={"Remove element"}>
|
||||
@@ -208,21 +209,22 @@ export const GaugeCard = ({data, width = "100%", additionalStyles, innerElement,
|
||||
})}
|
||||
>
|
||||
</Gauge>
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
export const CallbackDataCard = ({mainTitle, secondTitle, mainElement, secondaryElement, width="100%",
|
||||
editing, removeElement}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<div style={{
|
||||
marginRight: "5px",
|
||||
<Paper variant={"elevation"} elevation={1} style={{
|
||||
marginRight: "0.5rem",
|
||||
width: width,
|
||||
height: "100%",
|
||||
border: "1px solid gray",
|
||||
border: `1px solid ${theme.borderColor}`,
|
||||
borderRadius: "5px",
|
||||
overflow: "hidden",
|
||||
borderRadius: "4px",
|
||||
}} >
|
||||
<h5 style={{marginTop: 0, marginLeft: "5px", marginBottom: 0, paddingBottom: 0}}>
|
||||
<h2 style={{marginTop: 0, marginLeft: "0.5rem", marginBottom: 0, paddingBottom: 0}}>
|
||||
{editing &&
|
||||
<span >
|
||||
<MythicStyledTooltip title={"Remove element"}>
|
||||
@@ -233,7 +235,7 @@ export const CallbackDataCard = ({mainTitle, secondTitle, mainElement, secondary
|
||||
</span>
|
||||
}
|
||||
{mainTitle}
|
||||
</h5>
|
||||
</h2>
|
||||
<div style={{height: 180, cursor: "pointer"}}>
|
||||
<MythicStyledTooltip title={"Go to Active Callbacks"}>
|
||||
{mainElement}
|
||||
@@ -243,43 +245,46 @@ export const CallbackDataCard = ({mainTitle, secondTitle, mainElement, secondary
|
||||
{secondaryElement}
|
||||
</MythicStyledTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
export const TableDataCard = ({title, width = "100%", tableHead, tableBody, editing, removeElement, customizeElement}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<div style={{
|
||||
marginRight: "5px",
|
||||
<Paper variant={"elevation"} elevation={1} style={{
|
||||
marginRight: "0.5rem",
|
||||
width: width,
|
||||
height: "100%",
|
||||
border: "1px solid gray",
|
||||
borderRadius: "4px",
|
||||
border: `1px solid ${theme.borderColor}`,
|
||||
borderRadius: "5px",
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<h3 style={{marginTop: 0, marginLeft: "5px", marginBottom: 0, paddingBottom: 0}}>
|
||||
<h3 style={{marginTop: 0, marginLeft: "0.5rem", marginBottom: 0, paddingBottom: 0}}>
|
||||
{editing &&
|
||||
<span >
|
||||
<MythicStyledTooltip title={"Remove element"}>
|
||||
<IconButton onClick={removeElement} >
|
||||
<DeleteIcon color={"error"}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</span>
|
||||
<MythicStyledTooltip title={"Remove element"}>
|
||||
<IconButton onClick={removeElement} >
|
||||
<DeleteIcon color={"error"}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</span>
|
||||
}
|
||||
{title}
|
||||
<span style={{float: "right", marginRight: "5px"}}>
|
||||
<span style={{float: "right", marginRight: "0.5rem"}}>
|
||||
{customizeElement}
|
||||
</span>
|
||||
</h3>
|
||||
<div style={{height: 200, overflowY: "auto"}}>
|
||||
<Table style={{ "maxWidth": "100%", "overflow": "auto"}} stickyHeader size="small">
|
||||
<div style={{height: 200, overflowY: "auto", width: "100%"}}>
|
||||
<Table style={{ maxWidth: "100%", overflow: "auto"}} stickyHeader size="small">
|
||||
{tableHead}
|
||||
{tableBody}
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
export const LineTimeChartCard = ({data, additionalStyles}) => {
|
||||
const theme = useTheme();
|
||||
const [value, setValue] = React.useState([0, 0]);
|
||||
const [range, setRange] = React.useState([0, 0]);
|
||||
React.useEffect( () => {
|
||||
@@ -309,12 +314,12 @@ export const LineTimeChartCard = ({data, additionalStyles}) => {
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div style={{
|
||||
marginBottom: "5px",
|
||||
marginTop: "10px",
|
||||
<Paper variant={"elevation"} elevation={1} style={{
|
||||
marginRight: "0.5rem",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
border: "1px solid gray",
|
||||
border: `1px solid ${theme.borderColor}`,
|
||||
borderRadius: "5px",
|
||||
overflow: "hidden",
|
||||
}} >
|
||||
<Typography variant={"h3"} style={{margin: 0, padding: 0, position: "relative", left: "30%"}}>
|
||||
@@ -369,11 +374,12 @@ export const LineTimeChartCard = ({data, additionalStyles}) => {
|
||||
max={range[1]}
|
||||
sx={{ mt: 2, width: "80%", left: "10%" }}
|
||||
/>
|
||||
</div>
|
||||
</Paper>
|
||||
|
||||
)
|
||||
}
|
||||
export const LineTimeMultiChartCard = ({data, additionalStyles, colors=normalColors, view_utc_time, editing, removeElement, customizeElement}) => {
|
||||
const theme = useTheme();
|
||||
const [value, setValue] = React.useState([0, 0]);
|
||||
const [range, setRange] = React.useState([0, 0]);
|
||||
React.useEffect( () => {
|
||||
@@ -423,13 +429,13 @@ export const LineTimeMultiChartCard = ({data, additionalStyles, colors=normalCol
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div style={{
|
||||
<Paper variant={"elevation"} elevation={1} style={{
|
||||
width: "100%",
|
||||
marginRight: "0.5rem",
|
||||
height: "100%",
|
||||
marginRight: "5px",
|
||||
border: "1px solid gray",
|
||||
border: `1px solid ${theme.borderColor}`,
|
||||
borderRadius: "5px",
|
||||
overflow: "hidden",
|
||||
borderRadius: "4px",
|
||||
}} >
|
||||
<Typography variant={"h3"} style={{margin: 0, padding: 0, position: "relative", left: "30%"}}>
|
||||
{editing &&
|
||||
@@ -500,7 +506,7 @@ export const LineTimeMultiChartCard = ({data, additionalStyles, colors=normalCol
|
||||
max={range[1]}
|
||||
sx={{ mt: 2, width: "80%", left: "10%" }}
|
||||
/>
|
||||
</div>
|
||||
</Paper>
|
||||
|
||||
)
|
||||
}
|
||||
@@ -509,12 +515,14 @@ export const StackedBarChartCard = ({data, labels, title, width="100%", hidden,
|
||||
top: 40,
|
||||
bottom: 10,
|
||||
}}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<div style={{
|
||||
marginRight: "5px",
|
||||
<Paper variant={"elevation"} elevation={1} style={{
|
||||
marginRight: "0.5rem",
|
||||
width: width,
|
||||
height: "100%",
|
||||
border: "1px solid gray",
|
||||
border: `1px solid ${theme.borderColor}`,
|
||||
borderRadius: "5px",
|
||||
overflow: "hidden",
|
||||
}} >
|
||||
<h3 style={{marginTop: 0, marginLeft: "5px", marginBottom: 0, paddingBottom: 0, position: "absolute"}}>
|
||||
@@ -544,6 +552,6 @@ export const StackedBarChartCard = ({data, labels, title, width="100%", hidden,
|
||||
}
|
||||
}
|
||||
}} />
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import {CallbacksCard} from "./CallbacksCard";
|
||||
import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
|
||||
|
||||
|
||||
export function Home({me}) {
|
||||
return (
|
||||
<div style={{width: "100%", height: "100%", display: "flex"}}>
|
||||
<MythicPageBody>
|
||||
<CallbacksCard me={me} />
|
||||
</div>
|
||||
</MythicPageBody>
|
||||
);
|
||||
}
|
||||
Loaded 100 of 362 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user