Compare commits

...
512 Commits
Author SHA1 Message Date
Cody Thomas 96b24b48a7 Merge pull request #594 from je4yfc/integration
Generalized Support for external services across compose generation and port checks
2026-10-11 10:24:27 -05:00
Cody Thomas bdc722f6ce Merge pull request #589 from mjmaley/fix/cli-pasted-whitespace
Handle pasted whitespace in task arguments
2026-10-08 10:39:11 -05:00
Joseph Erdosy f9e71d0859 feat(cli): generalize external service handling in compose configuration and port checks
Signed-off-by: Joseph Erdosy <[email protected]>
2026-09-10 10:46:37 -04:00
Joseph Erdosy 742d878928 fix(cli): support external Nginx and create config dir for block lists
Signed-off-by: Joseph Erdosy <[email protected]>
2026-09-10 10:46:34 -04:00
Joseph Erdosy 01ea8bc896 feat(database): add external PostgreSQL support and configurable SSL mode (#587, #590)
Signed-off-by: Joseph Erdosy <[email protected]>
2026-09-10 10:46:31 -04:00
pwn 94418bcd52 fix(cli): fail fast on docker-compose failures instead of misleading health loop
The pty path in runDockerCompose returned the (nil) error variable on
nonzero exit, and start() discarded ServiceStart's error. Both together
let 'mythic-cli start' proceed into the UI probe loop after a failed
compose up, ending in os.Exit(1) with unrelated diagnostics (run-1.log).

- dockerComposeManager.runDockerCompose: return a descriptive error on
  nonzero exit in the pty path
- start: log.Fatalf on ServiceStart error so compose failures surface
  immediately

Signed-off-by: Joseph Erdosy <[email protected]>
2026-09-10 10:46:28 -04:00
Cody Thomas 3ae9c5fe18 Merge pull request #593 from je4yfc/cli-error-handling-fixes
fix(cli): fail fast on docker-compose failures in mythic-cli start
2026-09-07 10:57:19 -05:00
pwn 5e85a20d14 fix(cli): fail fast on docker-compose failures instead of misleading health loop
The pty path in runDockerCompose returned the (nil) error variable on
nonzero exit, and start() discarded ServiceStart's error. Both together
let 'mythic-cli start' proceed into the UI probe loop after a failed
compose up, ending in os.Exit(1) with unrelated diagnostics (run-1.log).

- dockerComposeManager.runDockerCompose: return a descriptive error on
  nonzero exit in the pty path
- start: log.Fatalf on ServiceStart error so compose failures surface
  immediately
2026-09-06 14:29:33 -04:00
mjmaley 5f46db6ede Handle pasted whitespace in task arguments 2026-09-02 16:56:36 -04:00
its-a-feature e37e057a89 process browser fix 2026-07-16 13:02:35 -07:00
github-actions 33a46d7db7 Bump Mythic Dockerfile tag to match release 'v3.4.0.61' 2026-07-01 16:06:33 +00:00
its-a-feature 540aec0f00 fixing filtering bug in process browser 2026-07-01 08:45:06 -07:00
github-actions 29be09eef4 Bump Mythic Dockerfile tag to match release 'v3.4.0.60' 2026-06-21 17:04:09 +00:00
its-a-feature a5a1a972d7 updating versioning 2026-06-21 09:09:15 -07:00
its-a-feature 82648e8241 addressing issues #563 #564 and #565 2026-06-21 09:04:45 -07:00
github-actions e4da2c887f Bump Mythic Dockerfile tag to match release 'v3.4.0.59' 2026-06-04 18:25:04 +00:00
Cody Thomas 39f44f7dec Merge pull request #557 from anuj-modi/internal-registry-support
Internal Registry Support
2026-06-04 12:54:16 -05:00
Anuj Modi 0f51584493 add some logs so users know Mirror is configured 2026-05-27 10:35:40 -04:00
Anuj Modi dc50ecc0dc refactor to one variable rather than 2 2026-05-26 16:06:20 -04:00
Anuj Modi 59211a2fbf Add support for Interal Registries for Core images and Agents 2026-05-21 15:46:53 -04:00
its-a-feature f8d355a29c updating single task view 2026-05-07 16:16:32 -05:00
github-actions 1da8fdcd2a Bump Mythic Dockerfile tag to match release 'v3.4.0.58' 2026-05-07 20:08:16 +00:00
its-a-feature 2dcc3b5699 fixing lazyQuery on single task view page 2026-05-07 14:47:22 -05:00
Cody Thomas 8deec59270 Merge pull request #548 from s1nc3ju1y/fix-jupyter-port-env
Pass JUPYTER_PORT into mythic_jupyter container
2026-04-28 13:58:22 -05:00
s1nc3ju1y 071abeaf74 Pass JUPYTER_PORT into mythic_jupyter container 2026-04-28 19:32:05 +08:00
its-a-feature 6eb53da6b5 slight UI updates, additional color choice added, some formatting 2026-04-16 17:39:11 -05:00
github-actions d605a2f82c Bump Mythic Dockerfile tag to match release 'v3.4.0.57' 2026-04-16 13:28:59 +00:00
its-a-feature 31db999945 updating version and readme 2026-04-16 08:07:33 -05:00
Cody Thomas 9ae193be76 Merge pull request #546 from Kargha/fix-missing-return-apitokengen
fix missing return in GenerateAPITokenWebhook
2026-04-16 07:59:01 -05:00
Andreas Persson fc731af819 fix missing return in GenerateAPITokenWebhook 2026-04-16 12:17:38 +09:00
Cody Thomas d21dbf293b Merge pull request #541 from its-a-feature/dependabot/go_modules/Mythic_CLI/src/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp-1.43.0
Bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp from 1.25.0 to 1.43.0 in /Mythic_CLI/src
2026-04-15 16:13:55 -05:00
dependabot[bot] b6d98b1794 Bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
Bumps [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp](https://github.com/open-telemetry/opentelemetry-go) from 1.25.0 to 1.43.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.25.0...v1.43.0)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <[email protected]>
2024-12-10 19:07:54 +00:00
its-a-feature 64a08d92ff adding optimizations for messages with many responses
also added a few UI fixes
2024-12-10 13:06:43 -06:00
github-actions 42b62058a3 Bump Mythic Dockerfile tag to match release 'v3.3.0.45' 2024-12-04 18:15:40 +00:00
its-a-feature 2d35ea1eec UI update to fix callback tokens rendering issue 2024-12-04 07:58:05 -10:00
github-actions 331b95805d Bump Mythic Dockerfile tag to match release 'v3.3.0.44' 2024-11-27 22:22:18 +00:00
its-a-feature b4aa736586 updating the _use_volume setting to false for new agent installs 2024-11-27 16:07:07 -06:00
github-actions ec2078d339 Bump Mythic Dockerfile tag to match release 'v3.3.0.43' 2024-11-27 20:59:29 +00:00
its-a-feature 16b6d33526 updated to not raise errors unnecessarily for c2 status updates 2024-11-27 14:43:57 -06:00
its-a-feature 96b05c9333 few UI tweaks on the payloads page for better output viewing 2024-11-27 14:23:28 -06:00
github-actions 83f81c9dca Bump Mythic Dockerfile tag to match release 'v3.3.0.42' 2024-11-26 20:31:47 +00:00
its-a-feature ab5c5a9768 updating actions for invite links 2024-11-26 14:16:50 -06:00
its-a-feature 56ef65276c updating command augment injection args 2024-11-26 14:09:44 -06:00
github-actions 467d948ac1 Bump Mythic Dockerfile tag to match release 'v3.3.0.41' 2024-11-26 19:48:02 +00:00
its-a-feature 82b335cded updates to invite links 2024-11-26 13:18:34 -06:00
github-actions f9417645d2 Bump Mythic Dockerfile tag to match release 'v3.3.0.40' 2024-11-22 16:45:17 +00:00
its-a-feature c8d6e9fa9e adding new features and some slight ui fixes 2024-11-22 10:23:14 -06:00
github-actions 8f7625c883 Bump Mythic Dockerfile tag to match release 'v3.3.0.39' 2024-11-18 21:06:45 +00:00
its-a-feature 2752e5fece updating file hosting processes 2024-11-18 14:51:53 -06:00
github-actions 8a96739120 Bump Mythic Dockerfile tag to match release 'v3.3.0.38' 2024-11-18 18:20:18 +00:00
its-a-feature 99f1e7a7a3 onStart container apitoken fix and slight UI tweak 2024-11-18 12:04:28 -06:00
its-a-feature 688b2ca946 re-adding non-git lfs files 2024-11-15 15:09:37 -06:00
its-a-feature 71ba45c2f9 removing .gitattributes on this branch 2024-11-15 15:06:03 -06:00
its-a-feature b2443d1ca2 updating react files to use git lfs 2024-11-15 14:25:07 -06:00
its-a-feature ade07381c1 updating gitattributes 2024-11-15 14:21:49 -06:00
its-a-feature 228c0ab57b updating git attributes for lfs on compiled react ui pages 2024-11-15 14:17:25 -06:00
github-actions 43a081c82d Bump Mythic Dockerfile tag to match release 'v3.3.0.37' 2024-11-15 03:09:54 +00:00
its-a-feature c51b900777 small ui tweak 2024-11-14 20:52:43 -06:00
github-actions 0cfb6b0ca7 Bump Mythic Dockerfile tag to match release 'v3.3.0.36' 2024-11-15 02:39:50 +00:00
its-a-feature 51cf1c9ff5 Save user preferences to database 2024-11-14 20:24:22 -06:00
github-actions 05ed8b14f1 Bump Mythic Dockerfile tag to match release 'v3.3.0.35' 2024-11-08 21:32:52 +00:00
its-a-feature 55c95cba5b slight ui fix 2024-11-08 15:17:17 -06:00
github-actions 5fae8eeb7d Bump Mythic Dockerfile tag to match release 'v3.3.0.34' 2024-11-06 21:16:37 +00:00
its-a-feature 8da0ed6ace one more tweak to the payload search 2024-11-06 15:01:29 -06:00
its-a-feature ab63e3862e updating to fix payload search bug 2024-11-06 14:56:48 -06:00
github-actions 6683e9853f Bump Mythic Dockerfile tag to match release 'v3.3.0.32' 2024-11-06 13:58:11 +00:00
its-a-feature fe59952384 few UI CLI tweaks 2024-11-06 07:42:43 -06:00
github-actions 96fe2e8a49 Bump Mythic Dockerfile tag to match release 'v3.3.0.31' 2024-11-05 21:08:33 +00:00
its-a-feature badb562ebe adding additional checks for operator status 2024-11-05 14:52:52 -06:00
its-a-feature 4d95dc8d36 fixing opsec check bypass request 2024-11-05 14:48:07 -06:00
github-actions 6b2de0c719 Bump Mythic Dockerfile tag to match release 'v3.3.0.29' 2024-11-05 17:36:48 +00:00
its-a-feature e1c37ba5ca UI updates and temp tweak for wrappers 2024-11-05 11:21:42 -06:00
github-actions ef79c90d75 Bump Mythic Dockerfile tag to match release 'v3.3.0.28' 2024-11-01 12:50:18 +00:00
its-a-feature 319a71598a fixing regression with screenshots added to file browser 2024-11-01 08:34:51 -04:00
512 changed files with 45908 additions and 32304 deletions

No files matched your search

+11 -9
View File
@@ -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
@@ -358,6 +358,8 @@ jobs:
# Pull in the repository code
- name: Checkout the repository
uses: actions/checkout@v4 # ref: https://github.com/marketplace/actions/checkout
with:
lfs: true
- name: Log in to the container registry
uses: docker/login-action@v3 # ref: https://github.com/marketplace/actions/docker-login
with:
@@ -380,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
@@ -438,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
@@ -550,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
+3
View File
@@ -167,3 +167,6 @@ _ReSharper*/
jupyter-docker/jupyter/MythicExamples/.ipynb_checkpoints/
nginx-docker/config/blockips.conf
/MythicReactUI/build/
/MythicReactUI/node_modules/
/.gocache/
+918
View File
@@ -4,6 +4,924 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.4.36] - 2026-06-21
### Changed
- Addressed 3 issues from GitHub about spectators and overly scoped hasura filters
## [3.4.35] - 2026-04-16
### Changed
- Merged PR #546 to address missing return in GenerateAPITokenWebhook that would allow non-admin users to generate API Tokens for Bot accounts
- Added server name to emitted log messages
- Merged PR #545 to address missing operation_id in task's log messages for logging containers
- Updated C2 profile code in the UI to properly handle `ui_position` from C2 Profile parameters in payload build
## [3.4.34] - 2026-04-13
### Changed
- Updated way that SendAllOperationsMessage functions to reduce duplicate messages to event log
- Reduced the number of lookups per error message to increment count by caching error lookup data
- Updated event log messages to ensure proper UTF8 encoding (and removal of bad characters)
- Using channels to ensure no duplicate messages are created due to go concurrency
## [3.4.33] - 2026-04-10
### Changed
- Fixed an issue where payload/command syncing would error out and leave dangling database connections
- Updated the postgres docker image to use customized database info for health checks
## [3.4.32] - 2026-04-06
### Changed
- Merged PR to add GET route for /auth_acs/:containerName/:IDPName that was POST only
## [3.4.31] - 2026-03-23
### Changed
- Removed code in `upload` to overwrite uploaded filename with destination filename in filemeta tracking
- Updated `download` code to only write files to disk on new chunks for speed improvements
- Updated the `rm` tracking to not blanket wildcard paths, but first check if it's a directory and make sure there's a trailing slash before wildcarding
## [3.4.30] - 2026-03-20
### Changed
- Added a `received_chunk_ids` map to the database for `filemeta` to track not just chunks, but which chunks are received
- this helps prevent duplicate chunks getting counted multiple times and causing faulty downloads
- Added some default values for `DisplayPath` that were causing null constraint errors on the database
- Fixed a remote path null value with uploading eventing files
## [3.4.29] - 2026-03-18
### Changed
- Adjusted the MythicRPCFileSearch function to not return tracked copies of existing files
## [3.4.28] - 2026-03-17
### Changed
- Fixed a bug where file transfers would fail if no remote path supplied
## [3.4.27] - 2026-03-16
### Changed
- Fixed a bug where mythic would try to write to a null message after processing a bad message
## [3.4.26] - 2026-03-12
### Changed
- Fixed a bug in "Callback Imports" that wouldn't recognize exising build parameter types
## [3.4.25] - 2026-03-04
### Changed
- Updated rpfwd to do burst proxy reads similar to socks
## [3.4.24] - 2026-02-18
### Changed
- Updated PushC2 to funnel traffic into one channel instead of two
- Updated socks proxy reads to do a basic burst for read sizes to reduce number of concurrent waiting messages
- this increases throughput for socks traffic with a bit more bursty memory usage
- Updated key reflection in agent messages to omit cwd and impersonation_context fields
- Updated empty BFS paths to be cached as well so non-existent paths aren't recomputed a bunch
## [3.4.23] - 2026-02-08
### Changed
- Updated the payload type syncing to resync for all payload types, not just new wrappers
- this fixes a bug where payload types syncing for the first time after their wrapper synced wouldn't match them up
## [3.4.22] - 2026-02-02
### Changed
- Fixed an issue where building payloads while c2 profiles were syncing would double issue tasks to c2 profile and hang
- Fixed an issue here cached p2p paths weren't getting flushed properly
## [3.4.21] - 2026-01-14
### Changed
- Fixed an issue where downloaded files weren't having their sizes reflected after completion in the file browser
## [3.4.20] - 2026-01-08
### Changed
- Updated IterateAndAct to treat a crypto value of "" the same as "none"
## [3.4.19] - 2025-12-31
### Changed
- Updated processing of skipped steps to indicate skip instead of most recent step that was updated
## [3.4.18] - 2025-12-23
### Changed
- Updated how processing happens for the `update_deleted` with file browser data
- data is now processed as part of two channels to ensure ordering and supporting multi-chunk file browser data
## [3.4.17] - 2025-12-23
### Changed
- Fixed an issue where uploaded files were getting treated as folders in the file browsers
- Fixed an issue where uploaded files would get the wrong timestamp in the file browser
- Updated go modules
## [3.4.16] - 2025-12-23
### Changed
- Fixed an issue where supplying a cron job in eventing as something other than string would crash due to type conversion
## [3.4.15] - 2025-12-22
### Changed
- Fixed an issue with translation containers sometimes not getting associated with their payload types on first sync
## [3.4.14] - 2025-12-18
### Changed
- Added database triggers for `filemeta`, `credential`, and `keylog` to update timestamp when other fields change
## [3.4.13] - 2025-12-15
### Changed
- Updated MythicRPCTaskCreate to require OperatorID, TaskID, or EventStepInstanceID
- This is an update to make sure the new task is associated with the right user and not the callback creator
- Updated the default tasking to Split Tasking instead of the collapsable accordian tasking
## [3.4.12] - 2025-12-11
### Changed
- Added more context to the Proxy start event log message
- Added a new event log message when a Proxy stops
- Updated the CustomBrowserExportFunctionMessage to also take in a callback group
- Updated the SendMythicRPCFileCreate call to also allow creation by OperationID
## [3.4.11] - 2025-12-10
### Changed
- Added `CustomBrowser` containers to allow customized file-like browser creation
- This includes an "export" functionality that's a custom function you can implement per browser type
- This includes a new field in `responses` for `custom_browser`
- Added `SendMythicRPCCallbackTokenSearch`
- Added `SendMythicRPCCustomBrowserSearch`
- Added `verifier_regex` option to CommandParameters
- Allows you to provide regex that must match the supplied value
- Changed `mythictree` to move `has_children` out of metadata in support of CustomBrowsers
- Changed `mythictree` to have new `display_path` field separate from `full_path` in support of CustomBrowsers
- Added field to `payload` to track the version of the backing payload_type when the payload was created
- This makes it easier to track payloads as you're doing payload type development/updates
- Added `supported_c2` and `supported_wrapping` to Payload Types to track supported C2/Wrappers
- This is now used to "resync" data rather than asking the container to resync back to Mythic
- Changed `token_id` to int64 (from uint64) to match similar change in Postgres database to BigInt from just Integer
- Updated operation event log auto resolver to use new `warining=true` vs `level='warning'` check
- Added a 5s sleep between container's OnStart messages to help with multiple operations bombarding a container in rapid succession
- Updated standard RPC timeouts to 10s instead of 5s
- Updated auto re-hosting of files to have a 10s delay between files
- Updated ContainerVersion out of bounds message to be a bit clearer
- Updated `SendMythicRPCCallbackUpdate` to allow specifying the `dead` attribute
- Updated `SendMythicRPCFileSearch` to allow specifying a PayloadUUID in addition to AgentFileID in `AgentFileID` field
- Updated `SendMythicRPCResponseCreate` to go through the same process as an agent's `user_output`
- This allows responses in this way to be intercepted and emitted to logging
- Added some mutexes when syncing PayloadTypes to prevent possible race conditions
- Updated file downloads to allow specifying a negative `total_chunks` until you're done and know how many chunks total there are
- Added new `database` tab to file previews to run SQL queries on database right in the browser
- Added the following new options for BuildParameter's HideConditionOperator
- LessThan, GreaterThan, LessThanOrEqual, GreaterThanOrEqual, StartsWith, EndsWith, Contains, NotContains
- [UI Updates](/MythicReactUI/CHANGELOG.MD#0383---2025-12-10)
- [CLI Updates](/Mythic_CLI/Changelog.md#0321---2025-12-10)
## [3.4.10] - 2025-10-31
### Changed
- Fixed an issue with c2 profile logos not getting reset if removed
## [3.4.9] - 2025-10-30
### Changed
- Fixed an issue with eventing where payload data wasn't getting passed to new callback triggers in some situations
## [3.4.8] - 2025-10-29
### Changed
- Added support for a new MythicRPCCallbackEdgeRemove RPC call to allow developers to selectively remove edges
- Added support for a new MythicRPCHandleAgentJson RPC call to allow developers to submit standard Mythic JSON messages here for processing after using a translation container instead of hitting a bunch of various other RPC calls to create their needed objects
- Updating MythicRPCCallbackEdgeSearch to also return self edges - i.e. an egress' edge to Mythic
- This is a change where before it would only return P2P edges in the graph
- Updated MythicRPCTaskCreate to take in either AgentCallbackID or CallbackID
- Added a few more spots in agent message processing to emit error information to the operation event log
## [3.4.7] - 2025-10-28
### Changed
- Added "ui_position" fields to BuildParameters and C2 Profile Parameters for display ordering when building payloads
- Added "dynamic_query_function" option to BuildParameters to allow dynamic execution and choice creation during build process
- Added "copy_of_file_id" to FileMeta to better tracking when Mythic creates copies of files
- When uploading fileA to callback1, Mythic tracks this association so it can record things like upload location
- If fileA (by same UUID) is uploaded again (callback1 with a different task or callback2), Mythic creates a copy of the file meta and tracks it
- This allows Mythic to track the new upload location separate from the first upload
- Both of these files are backed by the same file on disk, so deleting one deletes all copies
- This is now better represented in the UI on the file search page
## [3.4.6] - 2025-10-16
### Changed
- Adjusted how file browser permissions are treated by default
- they're left as-is and not force-converted to an array
## [3.4.5] - 2025-10-16
### Changed
- Fixed a bug where creating payloads via scripting without providing values wasn't picking up dictionary based payload type c2 parameter deviations
## [3.4.4] - 2025-10-16
### Changed
- Updated apitoken create mutation to also return creation_time
## [3.4.3] - 2025-10-13
### Changed
- Fixed an issue with the SendMythicRPCCallbackGraphEdgeSearch returning null data for the callbacks
## [3.4.2] - 2025-10-10
### Changed
- Fixed a bug with default dictionary values for scripting parameters
- Updated the send event log scripting call to account for mythic_sync and level=warning values
- Updated delegate tasking to not include the `token` field if it's 0
## [3.4.1] - 2025-10-09
### Changed
- Fixed an issue with download tags not getting added to files
- Updated last checkin time tracking to bulk update once/s instead of once/s per callback
- the goal is to minimize unnecessary database writes that might spike the cpu for repeated actions
- Updated to experimental golang json v2 for performance increases
## [3.4.0] - 2025-10-07
### Changed
- Added "selected_os" array option to some eventing triggers to additionally limit not just on payload type but OS as well
- this is the "os" you select when you build a payload
- Modified the UI to allow creating new eventing workflows based on existing ones with a single button
- Added group_name, supported_os, and hide_conditions to build parameters for more dynamic build experiences
- Added supports_multiple_c2_in_build, supports_multiple_c2_instances, c2_parameter_deviations to payload types
- This allows payload types to slightly tweak c2 profile configurations they support and indicating how many c2s are allowed per build
- c2 parameter deviations allow indicating parameters are not supported at all, that they have a different set of choices, and that the default value can be something else
- Added semver option for payload types to more accurately convey their current version outside of their description
- Updated the payload build workflow in the UI
- Added an option for payload types to hook into the `help` function and provide their own as part of their payload type definition
- Added a notification to callback tabs if there's new task changes while you're not on the tab
- Updated delegate tasks for the same target callback to be grouped together into a single message instead of a separate message per task
- Fixed a bug with MythicRPCCallbackSearch where all the timestamps would be 0
- Added support for C2 Profiles to include their own agent icons for the UI
- Added support for C2 Profiles to report their own SemVer as well
- Added support for TranslationContainers to report back their own SemVer
- Added support for all consuming containers (eventing, auth, logging, webhook) to report back their own SemVer
- Fixed bug in reporting where tasks weren't in order
- Updated the CredentialCreateWebhook to support empty credentials
- Added callback context menu option for triggering eventing workflows
- Also added option to bulk issue eventing workflows from callbacks
- Edited workflows so that all can be triggered manually
- Edited the server so manul execution can still provide arbitrary extra data, not just keyword execution
- Updated operationeventlog to have a dedicated `warning` field that's separate from the level
## [3.3.1-rc94] - 2025-09-05
### Changed
- Fixed a processing bug for staging_translation messages that were getting their messages overwritten
## [3.3.1-rc93] - 2025-08-27
### Changed
- Updated the global setting initializer to properly update from global state
## [3.3.1-rc92] - 2025-08-27
### Changed
- Fixed a bug where consuming containers were never getting their OnStart called
## [3.3.1-rc91] - 2025-08-24
### Changed
- Fixing bug with eventing causing issues with ints
## [3.3.1-rc90] - 2025-08-21
### Changed
- Updated translation container errors to include missing translation container name
- Added new global_setting table
- You can now add a default user preferences config that gets applied to all new users
- Added new invite_link table
- invite links are now persistent across reboots
- removed ability to delete invite links
- Added new `mythic_parsed_params` field to Task entries
- This shows how mythic parses the parameters from the UI
- This is viewable from a task, click to "view all parameters"
- Fixed some hasura operations to scope down to current operation only
- Added new `semver` (semantic version) field to payload types
- This will be updated in a later version to fetch data from updated pypi/go packages
- Added new TriggerData for the following triggers to allow scoping down to specific commands for specific agents
- TaskCreate, TaskStart, TaskFinish, TaskIntercept
- This takes a map[string][]string format (a dictionary of string arrays)
- The dictionary key is the name of the payload type, the array of strings are the specific commands to trigger on
- An empty array of strings means this applies for all commands for that payload type
- Added a check in handling messages from agents to check for the all 0 UUID
- This is now reported back in the UI as a special error to indicate that the connectivity was a success
- This works in tandem with updated C2 Profiles' sample messages to use all 0 UUID values
- A new keyword is added for responses, `events`, which is an array where you specify the `keyword` (string) and `data` (dictionary) for an eventing workflow you want to trigger
- File Hosting now auto checks and adds `/` if it was somehow missed
- Additional checks were added to make removing files easier in case the file was deleted
- Updated Eventing response intercept logic to cache existing workflows to reduce the overhead of fetching data for every response
- Updated File Hosting tag process to only add tags after C2 Profile returned success for hosting the file
- [UI Updates](/MythicReactUI/CHANGELOG.MD#0368---2025-08-21)
## [3.3.1-rc89] - 2025-08-11
### Changed
- Fixed the processing of strings/ints for required fields in create tasking eventing
## [3.3.1-rc88] - 2025-07-31
### Changed
- Fixed a bug in cron eventing that would continue even after error
## [3.3.1-rc87] - 2025-07-21
### Changed
- Updated syncing for payload types to only happen with new c2, not all c2
- Updated syncing for translation containers to only force sync payload types when translation container is new
## [3.3.1-rc86] - 2025-07-18
### Changed
- Updated the processing of new file tracking to auto mark the file as completed for uploads
## [3.3.1-rc85] - 2025-07-17
### Changed
- Updated processing of command_augment containers to auto add commands to callbacks on any sync
## [3.3.1-rc84] - 2025-07-07
### Changed
- Updated the processing for completion functions to not blow away subtask error codes
## [3.3.1-rc83] - 2025-07-03
### Changed
- Updated the create credential webhook to more uniquely check for duplicates by type and comment
## [3.3.1-rc82] - 2025-07-02
### Changed
- Updated the cookie value to have secure `false` but httpOnly `true` so that `http` only versions of the UI can still access images via cookies
## [3.3.1-rc81] - 2025-07-01
### Changed
- Updated the invite link code to have fallback mechanisms for determining the full link invite address
## [3.3.1-rc80] - 2025-07-01
### Changed
- Updated back-end to not try to double-add commands when building payloads and checking dependencies
## [3.3.1-rc79] - 2025-06-27
### Changed
- Updated the processing for PushC2 to track the number of connected clients and only mark disconnected when they all exit
## [3.3.1-rc78] - 2025-06-23
### Changed
- Added `payload_id` and `callback_id` as options for tags
- Added `supports_multiple_c2_in_build` and `supports_multiple_c2_instances_in_build` database fields for Payload Types
- This is prepping for a future update to allow Payload Types to identify if they support multiple c2 to be selected at build
## [3.3.1-rc77] - 2025-06-19
### Changed
- Updated the database to track if a task wants to reprocess with a new command or the original command
- This was originally updated for `Command Augmentation` containers to allow proper tracking for when
- the `CommandName` was changed for a new `CommandPayloadType`, but this also resulted in every time the
- `CommandName` was changed in tasking that the processing would continue under the new command, which isn't always ideal
- Now, only if you set `response.ReprocessAtNewCommandPayloadType` will the processing change to the new `CommandName`
- This means that CommandA that sets its `response.CommandName = commandB` and does a `process_response` field will have the message processed at `CommandA` still
- Setting the above AND `response.ReprocessAtNewCommandPayloadType` will have the `process_response` processed at `commandB` (specify the same payload type name is ok too)
## [3.3.1-rc76] - 2025-06-18
### Changed
- Updated processing for agent messages to support new MythicRPCHandleAgentJson function
- This allows developers to hook into more "standard" agent message processing from MythicRPC locations
- Updated bulk downloading to force host if none exists ("UNKNOWN") and removes trailing "." if no file extension exists
- Fixed an issue where sometimes a callback on checking in after getting hidden wouldn't re-appear in the active table
## [3.3.1-rc75] - 2025-06-11
### Changed
- Updated the processing of completion handlers for tasks to better account script only and child tasks
- Added `cwd` and `impersonation_context` to callbacks as part of their `checkin` and update messages
- Added new `callback` field to agent response dictionary
- this field allows you to update any fields that you'd normally set in your `checkin` message
- Added new field to tasks, `CommandParameterType`, to better track where processing should go if a task sets a new PayloadType as part of its processing
- An example would be `forge` updating the command name and container where processing should continue - this new payload type is also tracked
- Updated health check code for database to properly report success
## [3.3.1-rc74] - 2025-06-09
### Changed
- Updated the processing of commands for building payloads to include `dependencies` in a command's attributes
- if `command1` is a dependency for `command2`, and the user selected command2 but not command1 when building,
- then mythic will automatically add `command1` to the list of commands that need to be built into the agent
- an example would be `dependencies = ["command1"]`
## [3.3.1-rc73] - 2025-06-04
### Changed
- Updated the UI to allow creation of new Artifacts (that aren't tied to specific tasks)
- Updated payload updating information to a new GraphQL Action for:
- description, callback_alert, delete, and callback_allowed
- New data tracked for callback_allowed to optionally prevent new callbacks from payloads
- Updated Artifact processing
- Reporting an artifact will result in getting back an `id` for the artifact
- That `id` can then be sent in future artifact messages to update its `needs_cleanup` and `resolved` status
- Payloads now have a `callback_allowed` attribute
- this is `true` by default, but setting it to false (toggling in the UI on the payloads page or callback metadata) will prevent future new callbacks from that payload
- this feature was "implicitly" there when you delete a payload, but that is removed in favor of this new method
- A new UI element will show on the payload if it's not marked for allowing callbacks
## [3.3.1-rc72] - 2025-6-03
### Changed
- Updated the MythicRPCCallbackUpdate call to allow no specifying C2 Profile when asking to update last checkin time
## [3.3.1-rc71]- 2025-05-30
### Changed
- Updated `help` output to have `help` and `clear` alphabetically included
- Adding regex checks for `help`, so you can do `help list.*` to find all commands that start with `list` that are loaded
- File Uploads (Mythic -> Agent) now track and display chunks sent on the File Search page for Uploads
- State of the vertical navbar (open/close) is saved in user preferences so it'll stay through refresh
- Updated the "InviteLinks" UI button on the settings page to only be enabled if you're an admin and the feature is enabled
- Updated UI and GraphQL to allow bulk file delete
- Added new User Preference to hide browser-based tasking (file browser / process browser) from your normal tasking views
- Updated file media viewer to dynamically update as more chunks for a file are received
- Added a new `task:job_kill` supported UI feature that takes in a raw agent_task_id (uuid)
- This shows up as a `skull` icon for tasking that has been picked up by the agent but hasn't completed
- Updated UI callback drag columns to match vertical navbar color
## [3.3.1-rc70] - 2025-05-22
### Changed
- Updated the onStart operator selection to require the bot's current operation id to be the target operation
- Fixed a logic bug in remove callback_tokens
## [3.3.1-rc69] - 2025-05-21
### Changed
- Updated the logic for automatic eventing ingestion to not check for files that don't exist
## [3.3.1-rc68] - 2025-05-21
### Changed
- Updated the server to have the version baked-in instead of reading a file on disk
## [3.3.1-rc67] - 2025-05-19
### Changed
- Updated some processing of process data to update task_ids so that newer data gets updated in the UI
## [3.3.1-rc66] - 2025-05-14
### Changed
- Fixed a bug with processing a built payload's updated command list
-
## [3.3.1-rc65] - 2025-05-11
### Changed
- Fixed a bug with credential searching via MythicRPC
- Updated healthcheck to include details about all installed services
- Fixed issue in apitoken create for tasks
- Updated post response token action to be "add" if none specified
- Updated callbackToken action to be "add" if none specified
- Updated command action to be "add" if none specified
- Updated README sponsors
## [3.3.1-rc64] - 2025-04-15
### Changed
- Updated the payload build process to not double trigger c2 profile start
- Updated the logging for payload builds to be when the payload is done instead of when it starts
## [3.3.1-rc63] - 2025-04-10
### Changed
- Updated reporting backend to trim spaces on comma separated exclusion filters
## [3.3.1-rc62] - 2025-04-08
### Changed
- Updated the processing of timestamped for linked callbacks to not update nested relationships with egress connections
- Updated mythictree update/delete to update the associated task_id as well to trigger updates in the UI
- Added an `ORDER BY id DESC` for most MythicRPC Search calls
## [3.3.1-rc61] - 2025-04-07
### Changed
- Updated the socks bind to explicitly bind to IPv4 addresses
## [3.3.1-rc60] - 2025-04-04
### Changed
- Fixed typo in sql query
## [3.3.1-rc59] - 2025-04-03
### Changed
- Fixed an issue with the tag get or create rpc call's sqlx IN processing
## [3.3.1-rc58] - 2025-04-02
### Changed
- Fixed an issue with tag creation
## [3.3.1-rc57] - 2025-04-01
### Changed
- Updated invite link checking to not count invite usage if the account wasn't created
## [3.3.1-rc56] - 2025-03-28
### Changed
- Updated MythicRPCTaskSearch to allow specifying parent task to get all subtasks
- Fixed bug in TagCreateWebhook
- Added MythicRPCTagCreate
- Added MythicRPCTagSearch
- Added MythicRPCTagTypeGetOrCreate
- Added new trigger - tag_create
- Updated DynamicQueryFunction to take in OtherParameters - the names/values of other paramters in the task
- If a command has `task_response:interactive` in its supported ui features, then Mythic will open up a new interactive component in the UI
- If that command ALSO has `task:process_interactive_tasks` in their supported ui, then these follow-on tasks will also go to that parent task's create tasking function first
- If you set this, you can check for this type of task:
- `taskData.Task.IsInteractiveTask` - boolean True or False
- `taskData.Task.InteractiveTaskType` - the type of input (normal input, ctrl characters, etc)
- There's an enum, `InteractiveTask` that can provide insight into what the int TaskTypes mean
- `taskData.Args.GetCommandLine()` - get what they actually typed
- `taskData.Args.SetManualArgs("new args\n")` - potentially update the input to something else (don't forget `\n` if you need it!)
- Otherwise, follow-on tasks in this new interactive component go directly to the agent
## [3.3.1-rc55] - 2025-03-19
### Changed
- Updated processing of files in file browser for better handling of deleted files
## [3.3.1-rc54] - 2025-03-14
### Changed
- Updated the response intercept function to not trigger on empty user outputs
## [3.3.1-rc53] - 2025-03-13
### Changed
- Fixing struct tag for conditional eventing responses
## [3.3.1-rc52] - 2025-03-12
### Changed
- Updated processing of `clear` and `help` in event logs to properly send the data through
- Updated RPC timeout for RabbitMQ to 5s with 3 retries
- Updated RabbitMQ retry loop to include redeclaring channels to help avoid rabbitmq locks
## [3.3.1-rc51] - 2025-03-11
### Changed
- Updated the process of create tasking responses to allow arbitrary intermediate status values
## [3.3.1-rc50] - 2025-03-07
### Changed
- Updated the FileSearch to not implicitly check for files with comments
- Fixed an issue with task automation not properly returning the display_id for tasks
## [3.3.1-rc49] - 2025-03-05
### Changed
- Updated the `size` field in `filemeta` to `bigint` instead of `int` for larger file size tracking
- Added a timer to sync chunk counts every 5s
## [3.3.1-rc48] - 2025-03-05
### Changed
- Updated the create tasking webhook to return sooner before sending to containers
- Updated some of the rabbitmq connections with loops to be a bit more robust to errors
- Updated some of the tasking status messages to try to be a bit more informative
## [3.3.1-rc47] - 2025-02-26
### Changed
- Updated task output to support `stdout` and `stderr` fields in addition to `user_output`
## [3.3.1-rc46] - 2025-02-21
### Changed
- Updated some rabbitmq processing to reduce deadlocks and return quicker from timeout delays to the UI
## [3.3.1-rc45] - 2025-02-20
### Changed
- Updated rabbitmq with retries for failed tasks before aborting
## [3.3.1-rc44] - 2025-02-18
### Changed
- Fixed a trailing comma in a SQL query that broke creating files based on payload uuid
## [3.3.1-rc43] - 2025-02-14
### Changed
- Fixed a bug when downloading 0 byte files that would cause a crash when sending eventing notifications
## [3.3.1-rc42] - 2025-02-10
### Changed
- Just reverting the rabbitmq tweaks since the queues aren't getting cleaned up properly
## [3.3.1-rc41] - 2025-02-10
### Changed
- Few rabbitmq tweaks
## [3.3.1-rc40] - 2025-02-07
### Changed
- Updated the rabbitmq code to be more resilient to errors and delays
-
## [3.3.1-rc39] - 2025-01-30
### Changed
- Fixed a syncing bug that wasn't auto adding commands to new callbacks for command augmentation containers
## [3.3.1-rc38] - 2025-01-30
### Changed
- Updated callback creation for command augmentation payload types to not include deleted commands
- Updated callback command remove RPC to be more performant across large numbers of callbacks
- Fixed an issue with parameter type ChooseOneCustom that wouldn't save default values
- Updated file preview to give more meaningful error message if the file has been deleted from disk
## [3.3.1-rc37] - 2025-01-24
### Changed
- Updated the comment added to files automatically tracked due to a secondary task fetching them
- Updating the onStart code to not delay and automatically fire for containers
- Updated the MythicRPCTaskSearch and MythicRPCFileSearch commands due to broken logic
- Added fields for the MythicRPCCallbackUpdate to indicate if the last_checkin time should be updated
- This requires also specifying which c2 profile the callback is using
- UpdateLastCheckinTime and UpdateLastCheckinTimeViaC2Profile
- Updated the CommandAugment container sync code to automatically add commands to existing callbacks, not just new ones
- Updated container sync messages to the "debug" level instead of "info" level
- Updated the task's creation completion handler to append new stdout output instead of overwriting
## [3.3.1-rc36] - 2025-01-18
### Changed
- Added a new Eventing Trigger - callback_checkin that is based on a callback's new trigger_on_checkin_after_time attribute
- Added supporting functions for new GUI Eventing creation wizard
- Added new Eventing action - alert_create
- Added new Eventing action - webhook_send
- Added new EventLog level, 'api', that's used for information about scripting/eventing API usage
- Added new API function, eventingImportContainerWorkflow, to the GraphQL API that can be used by containers to import workflows in their onStart functionality
- Added ability to update certain pieces of an Eventing workflow after it's been registered
## [3.3.1-rc35] - 2024-12-28
### Changed
- Added a check in file download to return error if chunk num > 0 and no chunk data present
## [3.3.1-rc34] - 2024-12-23
### Changed
- Added the option to tag callbacks with specific colors via the UI
- Added another database field for future use to allow triggering on delayed callback checkins
- Added another database field for future use to track specific versions of payloadtypes installed
## [3.3.1-rc33] - 2024-12-17
### Changed
- Updated the login / refresh process to return the current UTC timestamp
- this is used in the UI to try to prevent clock skew on callback times
## [3.3.1-rc32] - 2024-12-14
### Changed
- Updated MythicRPCCallbackSearch to specify a list of payload types
- Updated MythicRPCCallbackAddCommand and MythicRPCCallbackRemoveCommand to take in a list of callback ids
## [3.3.1-rc31] - 2024-12-13
### Changed
- Added an optional flag for payloads syncing to not retrigger the container's on_start functionality
## [3.3.1-rc30] - 2024-12-12
### Changed
- Fixed an issue where dynamic query functions for command augmentation commands would go to the base agent instead of the right augmentation container
## [3.3.1-rc29] - 2024-12-10
### Changed
- Updated the processing for agent messages to minimize memory bloat
- Updated file download process to cache filemeta information and open file descriptors to minimize disk/database access
- Updated agent response processing to cache task data for one update instead of per-response
- Updated MythicRPCResponseCreate to also touch the timestamp of the corresponding task to update the UI streaming
## [3.3.1-rc28] - 2024-11-26
### Changed
- Updated the command_augment injection process to use suggested_commands as well as builtin commands
- one of these two attributes needs to be set to true on the command augment's command for it to be automatically injected into new callbacks
- this is in conjunction with matching OS requirements and matching the command augment's supported payload types (if specified)
## [3.3.1-rc27] - 2024-11-26
### Changed
- Added more functionality to invite links
- you can now specify a specific operation and role to assign the new operator to
- you can now specify (and change) the maximum number of usages that the invite link has
- Updated mythic-cli to properly pass in the environment's mythic_server_allow_invite_links setting
## [3.3.1-rc26]
### Changed
- Updated the automated payload creation process to associate a task_id with the payload
- Execute a container's OnStart method after a file has been removed/edited/created via the UI
- Added new parameter to the create_go_tasking response, ReprocessAtNewCommandPayloadType
- this allows you to set the response.CommandName to some other command and response.ReprocessAtNewCommandPayloadType to the same or different payload type
- execution will then pass to that payload type's create_go_tasking for the new `CommandName` .
- This allows execution chains to happen for processing
- Added two more parameters to the MythicRPCCallbackAddCommand and MythicRPCCallbackRemoveCommand functions
- AgentCallbackID - allows you to add to callback based on AgentCallbackID (UUID) instead of by TaskID
- PayloadType - allows you to specify the payload type associated with the commands if they different than the payload type for the callback itself
- This is helpful for command augment containers that want to register additional augment commands
- Using the UI to add/remove/update files in a container will re-rigger the container's onStart function
- This allows containers to reprocess data as needed if it changes on disk
## [3.3.1-rc25] - 2024-11-18
### Changed
- Updated how file hosting works for C2 profiles
- When syncing to Mythic, C2 profiles get updated file hosting based on existing FileHosted tags in Mythic
- When stopping file hosting, Mythic ensures that the C2 profile successfully processed the request before updating the corresponding Tag
## [3.3.1-rc24] - 2024-11-18
### Changed
- Fixed a bug with onStart messages getting the wrong value for temporary APITokens
- Increased the timeouts for gRPC messages from 1s to 10s
## [3.3.1-rc23] - 2024-11-14
### Changed
- Added a new field to file browser responses, `set_as_user_output` (bool) to indicate that this structured data should be turned into JSON and set as user_output data by Mythic
- This allows agents to send file browser data once, but get it counted for both the file browser and user_output
- Added endpoints to leverage/set user preferences
## [3.3.1-rc22] -2024-11-6
### Changed
- Payload Search wasn't properly filtering on filename or description
- Added an update to make sure that the payload is not deleted and the build phase was a success too
## [3.3.1-rc21] - 2024-11-5
### Changed
- Fixed an issue where bot accounts were considered as operators for opsec checks
## [3.3.1-rc20] - 2024-11-5
### Changed
- Fixed an issue where some Numbers were getting saved as scientific notation floats
- Temporary update where Wrappers can identify payloads to wrap, not just payloads identifying the wrappers
- This currently is only additive
## [3.3.1-rc19] - 2024-11-1
### Changed
- Updated the file download process to only add to the file browser if data is not a screenshot
## [3.3.1-rc18] - 2024-10-28
### Changed
+46 -1
View File
@@ -130,4 +130,49 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.
MIT license
===========
Copyright (c) 2017 sql.js authors (see AUTHORS)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# Some portions of the Makefile taken from:
Copyright 2017 Ryusei Yamaguchi
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
File diff suppressed because it is too large. Load diff
+26
View File
@@ -0,0 +1,26 @@
const webpack = require('webpack');
module.exports = function override(config, env) {
config.resolve.fallback = {
//url: require.resolve('url'),
fs: false,
assert: require.resolve('assert'),
crypto: require.resolve('crypto-browserify'),
path: require.resolve('path-browserify'),
//http: require.resolve('stream-http'),
//https: require.resolve('https-browserify'),
//os: require.resolve('os-browserify/browser'),
//buffer: require.resolve('buffer'),
stream: require.resolve('stream-browserify'),
};
/*
config.plugins.push(
new webpack.ProvidePlugin({
process: 'process/browser',
Buffer: ['buffer', 'Buffer'],
}),
);
*/
return config;
}
+3835 -5130
View File
File diff suppressed because it is too large. Load diff
+39 -32
View File
@@ -6,66 +6,73 @@
"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.10.5",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@apollo/client": "^3.13.8",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@mui/icons-material": "^5.15.20",
"@mui/lab": "^5.0.0-alpha.138",
"@mui/material": "^5.15.20",
"@mui/x-charts": "^6.19.8",
"@mui/x-data-grid": "^6.20.1",
"@mui/x-date-pickers": "^6.20.1",
"ace-builds": "^1.35.0",
"anser": "^2.1.1",
"dayjs": "^1.11.11",
"elkjs": "^0.8.2",
"graphql": "^16.8.2",
"graphql-ws": "^5.16.0",
"@hello-pangea/dnd": "^17.0.0",
"@mui/icons-material": "^7.0.2",
"@mui/lab": "^7.0.0-beta.11",
"@mui/material": "^7.0.2",
"@mui/utils": "^7.0.2",
"@mui/x-charts": "^8.1.0",
"@mui/x-data-grid": "^8.1.0",
"@mui/x-date-pickers": "^8.1.0",
"@xyflow/react": "^12.6.0",
"ace-builds": "^1.40.1",
"anser": "^2.3.2",
"dayjs": "^1.11.13",
"elkjs": "^0.10.0",
"graphql": "^16.10.0",
"graphql-ws": "^6.0.4",
"html-to-image": "^1.11.11",
"html2canvas": "^1.4.1",
"ip6": "^0.2.10",
"json5": "^2.2.3",
"jwt-decode": "^3.1.2",
"material-react-table": "^2.13.0",
"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": "^10.1.0",
"react-ace": "^14.0.1",
"react-colorful": "^5.6.1",
"react-dom": "^18.3.1",
"react-draggable": "^4.4.6",
"react-moment": "^1.1.3",
"react-router-dom": "^6.23.1",
"react-router-dom": "^7.13.1",
"react-scrollbar-size": "^5.0.0",
"react-split": "^2.0.14",
"react-toastify": "^9.1.3",
"react-virtualized": "^9.22.5",
"react-virtualized-auto-sizer": "^1.0.24",
"react-window": "^1.8.10",
"reactflow": "^11.11.3",
"react-toastify": "^11.0.5",
"react-tooltip": "^5.28.1",
"react-virtualized": "^9.22.6",
"react-virtualized-auto-sizer": "^1.0.26",
"react-window": "^1.8.11",
"save-svg-as-png": "^1.4.17",
"semver": "^7.6.2",
"styled-components": "^6.1.11",
"tough-cookie": "^4.1.4",
"sql.js": "^1.13.0",
"styled-components": "^6.1.17",
"uuid": "^9.0.1",
"webpack": "^5.94.0",
"webpack": "^5.105.0",
"word-wrap": "^1.2.5"
},
"scripts": {
"react-start": "CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=true WDS_POLLING=true BROWSER=none PORT=3000 WDS_SOCKET_PORT=7443 WDS_SOCKET_PATH=ws react-scripts start",
"react-build": "react-scripts build",
"react-test": "react-scripts test",
"react-start": "CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=true WDS_POLLING=true BROWSER=none PORT=3000 WDS_SOCKET_PORT=7443 WDS_SOCKET_PATH=ws react-app-rewired start",
"react-build": "react-app-rewired build",
"react-test": "react-app-rewired test",
"react-eject": "react-scripts eject",
"build": "npm run react-build",
"start": "cross-env BROWSER=none CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=true WDS_POLLING=true PORT=3000 WDS_SOCKET_PORT=7443 WDS_SOCKET_PATH=ws npm run react-start"
"start": "BROWSER=none CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=true WDS_POLLING=true PORT=3000 WDS_SOCKET_PORT=7443 WDS_SOCKET_PATH=ws npm run react-start"
},
"eslintConfig": {
"extends": [
+164
View File
@@ -0,0 +1,164 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.7.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Isolation_Mode" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 720 720" style="enable-background:new 0 0 720 720;" xml:space="preserve">
<style type="text/css">
.st0{fill:#E01F2F;}
</style>
<g>
<path class="st0" d="M631.71,544.4c-2.42-0.54-7.34-1.83-13.64-4.16c0.33,0.12,0.65,0.26,0.97,0.39
C624.37,542.61,628.45,543.68,631.71,544.4z"/>
<path class="st0" d="M633.4,544.75L633.4,544.75c-0.53-0.11-1.11-0.23-1.69-0.35C632.8,544.65,633.4,544.75,633.4,544.75z"/>
<path class="st0" d="M141.27,535.72C123.48,559.12,99,591.36,97.89,593.2c-1.18,1.97-7.99,13.54-20.39,18.46
c-17.32,5.6-21.78-2.86-21.78-2.86s-20.56,0.4-19.7,17.05h34.95v-0.02h33.31c0,0,0.58-14.58,15.16-17.49l60.92-48.82
C156.9,555.69,146.15,544.52,141.27,535.72z"/>
<g>
<path class="st0" d="M487.03,525.19l-76.29,72.09l72.21-76.16c-0.7-0.7-1.42-1.4-2.16-2.12c-9.15-8.9-17.68-13.07-26.83-13.07
c-10.91,0-22.71,5.71-37.09,13.4c2.02,14.66,2.11,33.84-7.43,46.78c-0.07,0.09-0.14,0.19-0.21,0.28
c-10.71,14.14-19.46,25.74-20.11,26.82c-1.19,1.98-8.11,13.75-20.72,18.61c-0.46,0.18-0.9,0.3-1.33,0.44
c-0.51,0.18-1.04,0.33-1.58,0.49c-0.7,0.19-1.4,0.37-2.07,0.49c-0.02,0-0.02,0-0.04,0c-11.13,2.07-16.38-4.44-16.38-4.44
s-20.56,0.4-19.7,17.05h34.95v-0.02h33.28c0,0,0.58-14.58,15.16-17.49c0,0,0,0,0.02-0.02c0.05-0.05,0.25-0.19,0.54-0.44l0.44-0.35
c6.99-5.78,49.59-41.09,73.3-66.94c3-3.27,5.69-6.39,7.96-9.27c-0.25-0.25-0.49-0.51-0.74-0.76
C490.59,528.89,488.89,527.1,487.03,525.19z"/>
<path class="st0" d="M384.77,207c0,0-0.02,20.99,7.18,27.11c7.2,6.15,22.41,1.46,32.15-9.76c11.84-13.64,18.61,5.09,18.61,5.09
c0.37-14.56-12.78-15.8-12.78-15.8c14.72-7.18,20.65,8.89,20.65,8.89c-0.67-7.2-4.21-15.58-16.17-15.56
c-16.51,0.04-18.79,17.6-33.3,20.07C386.6,229.51,384.77,207,384.77,207z"/>
<path class="st0" d="M681.85,338.39c-2.6-12.08-7.18-23.27-13-33.56c-5.83-10.29-13.01-19.69-21.41-28.04
c-8.38-8.34-18.05-15.77-29.27-21.3c-11.13-5.51-23.25-8.78-35.14-10.08c-11.92-1.3-23.62-0.74-34.79,0.84
c-11.15,1.6-21.88,4.23-31.96,7.08c-2.55,0.72-4.85,1.35-7.64,2.23c-1.35,0.42-2.67,0.79-4.04,1.26l-4.07,1.4
c-5.44,1.98-11.05,4.44-16.19,7.24c-1.32,0.68-2.53,1.39-3.76,2.09c-1.25,0.7-2.37,1.39-3.53,2.07l-6.46,3.88
c-4.11,2.46-8.08,4.64-12.19,6.71c-4.16,2.02-8.38,4-12.78,5.81l-3.28,1.37l-3.49,1.4l-6.92,2.74
c-9.15,3.56-18.16,6.76-27.22,9.32c-9.04,2.58-18.12,4.51-27.2,5.64c-9.08,1.12-18.14,1.4-27.13,0.74
c-1.12-0.09-2.25-0.14-3.37-0.23l-3.34-0.42l-3.35-0.42c-0.53-0.05-1.18-0.18-1.79-0.26l-1.81-0.28
c-4.78-0.77-9.4-1.62-13.96-2.63c-9.11-2.02-17.91-4.69-25.94-8.2c-8.03-3.51-15.24-7.92-20.67-13.21
c-5.46-5.3-8.94-11.31-10.55-18.25c-1.6-7.01-1.3-15.05,0.76-22.79c1-3.88,2.42-7.69,4.23-11.31c1.79-3.64,3.93-7.09,6.43-10.24
c2.48-3.18,5.25-6.08,8.31-8.57c3.04-2.51,6.32-4.69,9.82-6.39c1.72-0.91,3.58-1.51,5.36-2.28c1.86-0.53,3.69-1.23,5.62-1.56
c3.81-0.86,7.76-1.32,11.82-1.26c0.02,0,0.04,0,0.05,0c2.83,0.02,5.62,0.49,8.34,1.21c1.46,0.39,2.97,0.74,4.5,1.04
c3.93,0.77,7.39,1.58,10.47,2.44c0.23,0.09,0.46,0.18,0.68,0.26l0.04-0.07c3,0.84,5.62,1.72,7.87,2.58v-0.11
c0.86,0.37,1.69,0.76,2.46,1.12c0.02,0,0.02,0,0.04,0.02c10.78,5.22,12.05,9.69,12.05,9.69c0.04,0.09,0.09,0.12,0.12,0.21
c2.05,2.79,3.48,5.57,4.95,7.88c5.87,8.99,8.34,11.03,8.34,11.03l1.86-3.83c-7.55-8.34-9.78-32.65-9.78-32.65
s15.95-3.3,23.13,0.12c6.95,3.35,0.95,16.12,0.6,16.74c5.65-2.35,11.43-13.79,11.43-13.79c4.74-2.69,3.53-11.17,3.53-11.17
c-18.95-22.83-44.53-26.03-65.87-22.14c-2.48,0.04-4.92,0.14-7.34,0.33c-4.5,0.33-9.01,0.4-13.5,0.11
c-1.18-0.07-2.35-0.12-3.53-0.16c-6.09-0.18-12.29,0.39-18.37,1.65c-3.07,0.54-6.04,1.51-9.04,2.39
c-2.92,1.11-5.88,2.12-8.68,3.51c-5.65,2.65-10.94,6.02-15.77,9.87c-4.85,3.85-9.2,8.24-13.03,13c-3.86,4.74-7.18,9.89-9.96,15.3
c-2.79,5.41-5.04,11.1-6.67,17c-3.28,11.78-4.27,24.53-1.62,37.37c1.39,6.41,3.69,12.78,6.92,18.56
c3.21,5.8,7.27,11.01,11.73,15.51c4.48,4.51,9.34,8.34,14.36,11.63c5.02,3.28,10.2,6.06,15.44,8.45
c10.47,4.78,21.13,8.1,31.77,10.62c5.3,1.26,10.64,2.32,15.86,3.23l1.93,0.33c0.65,0.11,1.23,0.23,1.97,0.32l4.18,0.6l4.2,0.58
c1.4,0.14,2.81,0.25,4.21,0.37c11.22,0.97,22.5,0.76,33.56-0.44c22.14-2.37,43.22-8.82,62.78-16.17l7.25-2.76l3.58-1.39l3.74-1.49
c4.99-1.97,9.99-4.21,14.96-6.55c4.95-2.41,9.9-5.06,14.54-7.74c4.65-2.69,8.78-5.22,12.57-7.16c3.79-1.98,7.52-3.55,11.61-4.99
l3.13-1.02c1.04-0.35,2.16-0.63,3.23-0.95c2.05-0.61,4.65-1.3,6.97-1.93c9.45-2.55,18.49-4.58,27.41-5.74
c8.9-1.12,17.61-1.39,25.83-0.39c8.22,1,15.93,3.25,22.93,6.81c6.94,3.51,13.54,8.6,19.44,14.61
c5.88,6.01,11.06,12.92,15.12,20.32c4.07,7.38,7.09,15.21,8.68,22.99c1.55,7.76,1.6,15.38,0.02,22.23
c-1.6,6.85-4.71,12.98-9.52,18.32c-4.74,5.36-11.08,9.92-18.28,13.4c-7.2,3.48-15.26,5.9-23.39,7.11
c-4.06,0.61-8.15,0.91-12.12,0.88c-1,0.02-1.97-0.05-2.95-0.09l-2.81-0.28c-1.91-0.07-3.65-0.51-5.44-0.81
c-0.91-0.11-1.74-0.4-2.6-0.65c-0.86-0.26-1.72-0.44-2.58-0.74c-1.69-0.68-3.39-1.18-5.11-2.05l-1.3-0.58l-0.65-0.3l-0.67-0.37
l-2.67-1.44c-0.9-0.49-1.86-1.11-2.79-1.63l-1.4-0.84l-1.49-0.95c-3.86-2.42-8.29-5.37-13.01-8.25l-3.65-2.18
c-1.21-0.72-2.56-1.42-3.85-2.12c-2.55-1.44-5.36-2.72-8.1-4.02c-0.68-0.33-1.39-0.63-2.11-0.91l-2.14-0.86
c-1.44-0.56-2.86-1.18-4.32-1.67l-4.43-1.42c-1.48-0.47-2.95-0.95-4.46-1.26l-4.48-1.07c-0.76-0.18-1.49-0.35-2.25-0.51
l-2.27-0.39c-3-0.56-6.04-0.9-9.06-1.25c-6.08-0.54-12.15-0.67-18.18-0.37c-1.49,0.09-2.99,0.25-4.48,0.35l-2.25,0.19l-2.21,0.3
l-4.43,0.6l-4.36,0.81c-11.61,2.21-22.62,5.88-32.96,10.41c-5.18,2.25-10.19,4.76-15.08,7.39c-2,1.12-4.02,2.23-6.01,3.39
c-36.46,10.5-75.81,5.09-107.56-16.07c-0.84-0.56-1.72-1.16-2.6-1.76c-1.09-0.09-2.04-0.25-3.14-0.35
c-3.74-0.49-7.38-0.65-11.06-0.65c-4.23,0-8.38,0.4-12.42,1.09l-2.9,0.51l-2.99,0.74c-0.95,0.19-1.84,0.49-2.84,0.76
c-0.95,0.35-1.93,0.54-2.79,0.95c-3.85,1.3-7.48,3.09-11.12,4.99c2.9-2.95,6.18-5.55,9.87-7.57c3.74-2,7.64-3.76,11.71-4.85
c4.11-1.14,8.39-1.84,12.57-2.09c2.9-0.16,5.69-0.16,8.62,0c-6.22-7.73-19.79-23.18-31.14-27.08c0,0-39.74-30.17-52.4-39.72
c-12.71-9.68-27.17-27.13-24.94-32.86c0,0,9.57-13.96,17.89-16.54c8.27-2.7,10.47-12.28,10.47-12.28s-12.28,3.49-27.57,3.93
s-22.23,4.39-22.23,4.39s-8.78-7.04-18.81-8.34c0,0,0.84-30.15,40.64-49.4c39.78-19.18,86.49-44.5,104.42-98.2
c-1.49,2.35-24.97,39.6-53.28,50.1c-29.26,10.92-66.43,23.18-78.2,30.64c-11.85,7.43-34.56,32.35-47.7,79.13l-49.31,57.72
c0,0,2.58,6.97,6.94,6.53c0,0,1.3,9.57,10.01,5.62c0,0,2.23,10.08,0.46,14.01c-1.76,3.95,4.43,16.16,12.66,18.35l-0.79-4.79
c0,0,2.16,5.23,9.57,7.43c0,0-7.83-10.06-4.85-19.69c2.56-7.92,1.53-11.77,1.05-12.91l3.79,7.67c0,0,1.28-10.47-4.04-18.68
c1.93,2.49,11.36,12.92,31.26,5.94c1.79-1.62,3.49-3.28,5.04-4.99l2.18-2.53c0.76-0.81,1.32-1.76,2-2.65
c1.39-1.7,2.49-3.58,3.83-5.43c0.6-0.95,1.16-1.93,1.7-2.95c0.6-0.95,1.19-1.93,1.9-2.88c1.11-2,2.25-3.99,3.6-6.04
c-0.16,2.39-0.49,4.74-0.9,7.08c-0.39,1.19-0.65,2.35-0.98,3.49l-0.65,1.69c-0.21,0.61-0.3,1.16-0.6,1.7
c-0.49,1.09-0.98,2.23-1.55,3.28c-0.49,1.14-1.09,2.12-1.84,3.2c-0.6,0.98-1.23,2.09-1.93,3.04l-2.35,2.83
c-1.69,1.84-3.58,3.6-5.57,5.15c-0.46,4.23-1.7,10.22-4.39,15.73c-0.39,0.91-0.79,1.76-1.14,2.55c1.19-1,24.15-19.55,60.08-3.06
l1.9,0.79l-0.98,1.84l-2.65,4.58c4.79-1.14,9.68-1.19,14.47-0.11c6.41,1.55,11.77,6.09,15.3,11.27c3.69,5.2,5.74,11.22,7.48,17
c0.84,2.9,1.79,5.83,3.42,8.24c1.65,2.44,4.39,4.04,7.48,4.5c-3.09-0.05-6.23-1.35-8.18-3.9c-2.05-2.49-3.2-5.43-4.34-8.24
c-2.2-5.72-4.39-11.41-8.1-16.03c-3.58-4.6-8.38-8.24-13.91-9.54c-5.53-1.3-11.66-0.49-16.74,1.7l-4.95,2.09l2.55-4.74l3.79-6.83
c-7.43-3.04-15.4-4.64-23.32-3.9c-8.62,0.76-16.95,3.99-24.43,8.57c-0.05,0.05-0.11,0.09-0.16,0.16
c-9.71,5.83-17.4,14.45-22.93,24.32c-6.08,10.82-13.91,25.13-19.14,35.6c-9.68,19.19-18.35,22.74-18,31.45
c0.46,8.73-4.34,38.88-7.78,45.03c0,0,0.79,9.68,10.87,8.71c0,0,0,7.48,10.01,6.57c0,0-6.13-9.17,2.69-13.56
c8.78-4.34,14.4-0.39,16.56,4.79c2.2,5.27-4.79,12.85-10.22,8.82l-2.44-3.99c0,0-6.97,11.36,6.53,14.42c0,0,1.35,7.92,15.35,7.48
c0,0-7.87-3.93-1.69-10.08c0,0,10.41-3.93,8.32-23.62c0,0,0.25-1.4,1.19-2.25c0.14-1.88,0.32-3.78,0.65-5.62
c0.6-3.99,1.33-7.88,3.23-11.82c0.86-1.93,2.05-3.99,4.25-5.57l0.65-0.46l0.54-0.35c0.3-0.25,0.65-0.46,1.04-0.79
c0.56-0.49,1.35-1.05,1.84-1.69c2.49-2.55,4.69-5.58,6.48-8.87c1.98-3.2,3.69-6.69,5.18-10.19c-0.3,3.9-1.28,7.64-2.6,11.38
c-1.28,3.74-3.04,7.38-5.62,10.66c-0.65,0.84-1.35,1.63-2.11,2.39c-0.39,0.39-0.79,0.68-1.23,1.11l-0.6,0.49l-0.35,0.25
l-0.19,0.16c-0.81,0.74-1.62,2.12-2.25,3.74c-1.25,3.14-2.05,6.78-2.55,10.41c-0.35,2.2-0.6,4.34-0.79,6.48
c0.76,0.7,1.49,1.6,2.35,2.74c7.13,9.78,0.79,25.18,23.72,36.46c-0.84-2.09-1.69-4.28-2.42-6.48c-2.3-7.08-3.85-14.56-4.23-22.02
c-0.26-3.79-0.21-7.57,0-11.38c0.19-3.74,0.79-7.48,1.39-11.22c0.44-1.84,0.74-3.74,1.19-5.53l0.68-2.79l0.81-2.69
c0.54-1.79,1.19-3.6,1.88-5.37l0.79-2.55c0.37-0.79,0.56-1.6,0.81-2.49c1.93-6.73,2.83-13.77,1.9-20.63
c-0.9-6.78-3.49-13.4-7.78-18.79c-0.98-1.35-2.14-2.65-3.42-3.79c-1.25-1.14-2.65-2.16-4.09-3.09c-1.46-0.9-2.99-1.6-4.64-2.14
c-1.55-0.51-3.2-0.79-4.9-0.84c-3.55-0.16-6.94,0.49-10.03,2.35c-1.49,0.9-2.9,2.04-4.04,3.39c-1.25,1.3-2.35,2.84-3.06,4.48
c0.91-3.53,3.06-6.88,6.2-9.27c2.93-2.42,6.92-3.74,10.85-3.93c1.95-0.09,3.93,0,5.9,0.46c1.93,0.46,3.88,1.11,5.62,2.09
c1.76,0.84,3.44,2,4.93,3.2c1.55,1.23,2.99,2.6,4.28,4.04c2.65,2.95,4.83,6.29,6.34,9.87c1.69,3.58,2.9,7.38,3.34,11.26
c1.14,7.78,0.39,15.66-1.95,23.04c-0.25,0.9-0.54,1.83-0.84,2.74l-0.98,2.65c-0.61,1.7-1.25,3.39-1.76,5.09
c-1.16,3.39-2.09,6.88-2.9,10.38c-0.65,3.49-1.39,7.02-1.74,10.62c-0.3,3.53-0.6,7.18-0.49,10.76c0,3.6,0.19,7.18,0.54,10.76
c0.39,3.6,1.09,7.13,1.83,10.71c0.65,2.79,1.51,5.57,2.35,8.32c0,0.05,0.05,0.05,0.05,0.05s0,0.05,0.05,0.16
c1.33,4.13,3.09,8.11,5.13,11.98c2.79,3.88,6.94,8.08,13.26,11.56c6.34,3.42,14.82,6.27,26.18,7.32
c41.97,3.93,41.06,13.12,46.77,20.55c0,0,3.93-15.26-4.79-22.74c0,0,6.08-0.46,21.39,14.86c15.3,15.31,25.41,7.88,25.41,7.88
s-11.45,1.3-16.6-7.88c-5.27-9.17-7.52-30.56-14.51-34.93v-1.79c-1.09-1.25-2.2-2.55-3.35-3.9c-2.55-3.14-4.83-6.43-7.13-9.87
c-2.12-3.39-4.23-6.92-5.83-10.85c3.49,2.39,6.53,5.2,9.48,8.01c2.35,2.41,4.53,4.85,6.73,7.38c2.2,2.6,4.28,5.23,6.23,8.04
c3.14,4.53,5.9,9.11,8.13,14.36h0.05c13.72,17.44,32.15,43.62,28.57,55.07c-1.55,4.74-3.79,7.64-6.18,9.38
c-6.78,4.88-15.26,0.65-15.26,0.65s-20.58,0.44-19.69,17.05h62.5c0,0,7.43-10.52,16.26-13.96c8.62-3.51-19.37-72.54-19.74-73.48
c0.67,0.39,26.39,15.45,73.86-6.53c48.12-22.28,72.19-48.89,104.89-17.03c32.75,31.91,21.83,34.09,121.54,53.79
c0,0,34.1,13.12,19.65,38.88c0,0,0.11,0.44,0,1.09v-1.09c-10.22,9.15-16.4,1.3-16.4,1.3s-20.55,0.44-19.63,17.05h67.59
c0,0-1.84-10.52,7.32-13.96c9.24-3.53-10.06-27.97-16.16-50.29c-4.58-16.89-4.53-14.75-15.35-16.86c0,0-0.02,0-0.04-0.02
c-0.02,0-0.04-0.02-0.07-0.04c-0.05,0-0.16,0-0.25-0.05h-0.25c-0.25-0.05-0.6-0.16-1.09-0.25h-0.05c-0.21-0.05-0.4-0.09-0.61-0.16
c-0.11-0.02-0.19-0.05-0.3-0.07s-0.23-0.05-0.35-0.07c-0.05,0-0.11,0-0.19-0.05c-0.11,0-0.21-0.05-0.25-0.05
c-0.05-0.04-0.09-0.04-0.16-0.05c-0.02,0-0.04-0.02-0.05-0.02s-0.04-0.02-0.04-0.02c-0.04-0.02-0.04-0.02-0.07-0.04
c-0.05,0-0.09-0.05-0.19-0.05h-0.11c-0.09-0.05-0.19-0.05-0.3-0.05c-0.7-0.21-1.55-0.4-2.41-0.65c-0.09,0-0.16-0.05-0.25-0.05
c-0.11,0-0.16-0.05-0.25-0.09c-0.26-0.09-0.53-0.18-0.81-0.26c-0.19-0.05-0.39-0.11-0.56-0.18c-0.18-0.07-0.37-0.11-0.54-0.18
c-0.18-0.05-0.37-0.11-0.54-0.18c-0.3-0.11-0.6-0.19-0.91-0.26c-1.14-0.4-2.46-0.79-3.74-1.3c-0.16-0.11-0.33-0.16-0.49-0.25
c0-0.02-0.02-0.02-0.02-0.02h-0.02c-0.11-0.04-0.18-0.05-0.23-0.09c-0.04-0.02-0.07-0.04-0.11-0.05c0,0,0,0-0.02-0.02
s-0.05-0.04-0.09-0.05c-8.9-3.34-20.58-8.85-31.89-17.65c0,0-0.04-0.04-0.11-0.09c-3.9-3.04-7.76-6.44-11.43-10.29
c-0.04-0.04-0.07-0.07-0.11-0.11c-13.26-12.8-34.72-36.3-47.31-64.59c-0.35-0.79-0.74-1.58-1.12-2.35c0-0.02-0.02-0.02-0.02-0.02
c-3.25-6.37-7.92-12.15-13.49-17.37c-0.3-0.28-0.6-0.56-0.9-0.83c0.02,0,0.04,0.02,0.05,0.02c-4.79-4.69-10.1-8.9-15.61-12.66
l2.81,0.84c0.93,0.3,1.84,0.7,2.77,1.05l1.39,0.53c0.46,0.16,0.93,0.35,1.39,0.58c1.83,0.88,3.67,1.63,5.53,2.69
c0.93,0.51,1.84,0.95,2.81,1.51l2.88,1.69c0.95,0.53,2,1.21,2.99,1.83l3.09,1.91l3.21,2.02l3.44,2.12
c9.15,5.71,21.35,11.94,34.28,14.31l4.81,0.83c1.62,0.32,3.18,0.35,4.78,0.51l4.76,0.37c1.56,0.04,3.13,0.11,4.65,0.07
c6.16-0.02,12.2-0.56,18.16-1.53c11.87-1.93,23.43-5.51,34.4-10.98c5.5-2.72,10.82-5.97,15.89-9.75
c5.08-3.76,9.85-8.15,14.14-13.1c4.3-4.93,8.08-10.5,11.08-16.47c3-5.99,5.25-12.35,6.64-18.83
C684.92,363.52,684.41,350.44,681.85,338.39z M379.22,175.86l15,10.55l-23.46-8.52L379.22,175.86z M85.99,279.4l11.61-13.42h8.43
L85.99,279.4z M150.4,270.47l5.6-12.15l29.31-10.52l-23.99,11.36L150.4,270.47z M70.99,449.54l11.61-13.36h8.52L70.99,449.54z
M170.54,512c-3.69-5.58-5.64-12.36-6.04-19.11c-0.25-3.34-0.05-6.73,0.35-10.06c0.25-0.86,0.35-1.7,0.54-2.6
c0.19-1.05,0.4-1.6,0.56-2.44c0.44-1.49,0.68-2.99,0.98-4.53c1.14-6.18,1.69-12.52,1.79-18.81c0.32-12.61-1.39-25.32-4.64-37.48
c-0.84-3.09-1.84-6.04-2.99-8.92c-1.14-2.95-2.6-5.74-4.23-8.32c-1.7-2.65-3.69-5.08-6.13-7.02c-1.25-0.95-2.53-1.81-3.99-2.49
c-1.44-0.65-2.95-1.16-4.53-1.25c3.2-0.46,6.59,0.65,9.48,2.35c1.74,0.98,3.39,2.23,4.79,3.6c0.65-1.46,1.19-2.9,1.44-4.41
c0.35-1.53,0.44-3.09,0.44-4.64c-0.09-3.04-0.79-6.04-1.93-8.87c-1.09-2.84-2.6-5.58-4.34-8.18c-1.4-2.18-2.95-4.34-4.65-6.27
l-2.48,0.81c-1.25,0.46-2.55,0.95-3.83,1.39c-1.26,0.54-2.46,1.09-3.69,1.65l-1.7,0.98c-0.6,0.3-1.19,0.6-1.79,1
c-2.25,1.39-4.28,3.04-5.94,5.09c-0.51,0.54-0.84,1.09-1.16,1.63c-0.25,0.3-0.46,0.6-0.6,0.9c-0.09,0.3-0.35,0.6-0.44,0.91
c-0.54,1.23-0.9,2.53-1.05,3.99c-0.3-1.4-0.3-2.9-0.11-4.36c0.11-0.68,0.35-1.44,0.54-2.12c0.35-0.65,0.6-1.46,0.95-2.05
c0.74-1.39,1.55-2.53,2.6-3.74l1.44-1.63c0.61-0.49,1.16-0.95,1.76-1.46c0.54-0.54,1.19-0.93,1.74-1.33l1.84-1.3
c1.25-0.65,2.55-1.46,3.83-2.05c1.39-0.6,2.69-1.25,4.04-1.76l4.09-1.44l1.35-0.46l1,1.04c1.28,1.35,2.25,2.65,3.28,3.99
c1.05,1.35,2,2.74,2.79,4.2c1.79,2.88,3.2,6.02,4.13,9.41c0.4,1.6,0.79,3.28,0.9,5.04c0.16,1.74,0.05,3.49-0.16,5.18
c-0.35,1.69-0.79,3.34-1.49,4.88c-0.81,1.46-1.7,2.84-2.76,4c0.98,0.95,1.84,1.9,2.76,2.97c2.12,2.6,3.88,5.44,5.43,8.43
c1.55,2.95,2.84,6.09,3.88,9.17c2.2,6.29,3.69,12.71,4.76,19.25c0.95,6.53,1.33,13.15,1.14,19.79
c-0.16,6.62-0.98,13.29-2.46,19.79c-0.35,1.6-0.9,3.2-1.39,4.83c-0.19,0.76-0.54,1.69-0.68,2.2c-0.19,0.68-0.35,1.49-0.46,2.2
c-0.65,2.99-1,6.11-1,9.22c0,3.09,0.35,6.18,1.05,9.18c0.65,3.09,1.63,5.99,3.09,8.82c1.44,2.74,3.28,5.34,5.43,7.67
c2.11,2.34,4.53,4.5,7.18,6.32C179.46,522.03,174.23,517.67,170.54,512z M207.58,458.21c0.25,2.49,0.76,4.97,1.35,7.57
c0.65,2.48,1.6,4.93,2.53,7.48c-2.04-1.79-3.79-4.04-5.18-6.43c-1.35-2.44-2.41-5.02-3.2-7.81c-0.68-2.69-1.04-5.5-1.23-8.29
c-0.16-2.83,0-5.62,0.35-8.36c0.6-5.6,2.2-10.98,4.34-16c2.2-4.99,4.83-9.78,8.53-13.91c-1.49,5.18-3.04,10.26-4.25,15.4
c-1.23,5.04-2.18,10.12-2.9,15.21C207.32,448.1,207.18,453.17,207.58,458.21z M601.46,548.79c5.23,0.49,10.36,1.46,15.44,2.9
c2.49,0.76,4.99,1.65,7.34,2.79c2.39,1.19,4.69,2.6,6.73,4.43l1.46,1.55c0.39,0.49,0.74,0.98,1.19,1.49
c0.79,1.05,1.65,2.11,2.46,3.2c1.49,2.09,3.04,4.23,4.37,6.5c2.65,4.48,5.08,9.11,7.18,14l-0.19,0.11
c-3.2-4.2-6.09-8.53-8.97-12.82l-4.36-6.48c-0.65-1.09-1.39-2.14-2.12-3.25c-0.4-0.49-0.81-1.04-1.11-1.6l-1.14-1.35
c-1.84-1.74-3.9-3.23-6.13-4.37c-2.3-1.25-4.64-2.23-7.13-3.09c-4.83-1.74-9.87-2.93-15.07-3.74L601.46,548.79z M438.98,462.85
c-1.6,0.95-3.04,2.09-4.39,3.28c-1.35,1.14-2.6,2.55-3.69,3.99c-0.6,0.65-1.05,1.39-1.55,2.16c-0.46,0.76-0.9,1.55-1.19,2.23v0.05
l-0.16,0.21c-2.55,4.07-5.34,8.08-8.59,11.68c-1.39,1.93-3.18,3.64-4.88,5.43c-0.84,0.84-1.69,1.6-2.6,2.49
c-0.95,0.79-1.74,1.63-2.74,2.39c-7.39,6.34-15.72,11.52-24.53,15.51c-8.78,4.04-18.16,6.78-27.57,8.52
c-4.79,0.84-9.48,1.46-14.29,1.65c-2.46,0.16-4.85,0.19-7.24,0.16c-2.35-0.05-4.69-0.16-7.04-0.46
c9.41-1.05,18.65-2.84,27.61-5.34c4.55-1.19,8.92-2.69,13.36-4.28c4.28-1.6,8.62-3.34,12.77-5.39
c8.32-3.99,16.24-8.82,23.37-14.59c7.24-5.72,13.66-12.42,19.44-19.74c1.05-1.79,2.25-3.2,3.64-4.55
c1.35-1.44,2.95-2.69,4.58-3.74c1.65-1.09,3.34-1.98,5.15-2.63c1.79-0.76,3.74-1.16,5.57-1.51c-0.84,0.4-1.79,0.7-2.58,1.19
C440.67,461.95,439.77,462.35,438.98,462.85z M507.47,503.47l2.49,3.6c0.84,1.11,1.6,2.28,2.49,3.39l2.65,3.34l1.39,1.65
l1.39,1.49c1.02,1.05,2,2.05,2.99,3.09l3.09,2.84c1.09,1.05,2.2,1.9,3.28,2.79c1.09,0.9,2.2,1.79,3.39,2.63
c4.53,3.39,9.43,6.39,14.51,8.94c5.18,2.55,10.57,4.74,16.16,6.18l-0.16,0.54c-5.8-0.98-11.38-2.79-16.7-5.09
c-5.34-2.23-10.57-4.97-15.44-8.22c-1.32-0.68-2.49-1.63-3.71-2.49c-1.19-0.9-2.34-1.69-3.48-2.67l-3.35-2.84
c-1.09-0.93-2.2-1.98-3.28-2.99l-1.55-1.53l-1.49-1.65l-3.04-3.28c-0.95-1.14-1.81-2.28-2.69-3.49l-2.69-3.42l-1.3-1.65
l-1.39-1.55c-0.86-1.09-1.84-1.98-2.79-2.99c-2.05-1.83-4.18-3.64-6.43-5.18c-2.3-1.62-4.58-2.95-7.08-4.23
c-2.39-1.25-4.88-2.46-7.52-3.39c-10.27-4.04-21.58-5.9-33.12-6.2l-0.09-0.6c5.69-1.55,11.63-2.05,17.54-1.81
c2.93,0.11,5.99,0.46,8.87,0.91c2.99,0.49,5.94,1.3,8.82,2.2c1.4,0.46,2.79,0.98,4.2,1.55c1.39,0.6,2.79,1.23,4.14,1.98
c1.33,0.65,2.58,1.46,3.93,2.3c0.6,0.39,1.25,0.84,1.9,1.23c0.61,0.51,1.21,0.95,1.81,1.4C500.04,494,504.13,498.55,507.47,503.47
z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

+190 -7
View File
@@ -3,28 +3,207 @@ import {restartWebsockets} from "./index";
import {snackActions} from "./components/utilities/Snackbar";
export const meState = makeVar({loggedIn:false, user: null, access_token: null, refresh_token: null});
export const menuOpen = makeVar(false);
export const alertCount = makeVar(0);
export const taskTimestampDisplayFieldOptions = [
{
name: "timestamp",
display: "Latest Timestamp for anything task related"
},
{
name: "status_timestamp_preprocessing",
display: "When Operator Submitted Task"
},
{
name: "status_timestamp_processing",
display: "When Agent Picked up Task",
}
]
export const 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,
fontFamily: "Verdana, Arial, sans-serif",
showMedia: true,
hideUsernames: false,
showIP: false,
showHostname: false,
showOPSECBypassUsername: false,
showCallbackGroups: false,
useDisplayParamsForCLIHistory: true,
interactType: "interactSplit",
taskTimestampDisplayField: "timestamp",
callbacks_table_columns: ["Interact", "Host", "Domain", "User", "Description", "Last Checkin", "Agent", "IP", "PID"],
callbacks_table_filters: {},
autoTaskLsOnEmptyDirectories: false,
hideBrowserTasking: false,
hideTaskingContext: false,
taskingContextFields: ["impersonation_context", "cwd"],
["experiment-responseStreamLimit"]: 200,
sideShortcuts: defaultShortcuts,
palette: {
primary: {
dark: "#75859b",
light: "#75859b",
},
error: {
dark: '#bd5142',
light: '#c42c32'
},
success: {
dark: '#85b089',
light: '#0e7004',
},
secondary: {
dark: '#bebebe',
light: '#a6a5a5'
},
info: {
dark: '#84b4dc',
light: '#4990b2'
},
warning: {
dark: "#dc8455",
light: "#ffb74d",
},
background: {
dark: '#282828',
light: '#f6f6f6'
},
paper: {
dark: '#282828',
light: '#ececec'
},
tableHeader: {
dark: '#484848',
light: '#c4c4c4'
},
tableHover: {
dark: "#3c3c3c",
light: "#e8e8e8",
},
pageHeader: {
dark: '#1b2025',
light: '#706c6e'
},
text: {
dark: "#e4e4e4",
light: "#000000",
},
selectedCallbackColor: {
dark: '#26456e',
light: '#c6e5f6',
},
selectedCallbackHierarchyColor: {
dark: '#273e5d',
light: '#deeff8',
},
backgroundImage: {
dark: null,
light: null
},
navBarIcons: {
dark: '#ffffff',
light: '#ffffff'
},
navBarText: {
dark: '#ffffff',
light: '#ffffff'
},
navBarColor: {
dark: "#194573",
light: "#3b606d",
},
navBarBottomColor: {
dark: "#330814",
light: "#283581",
},
taskPromptTextColor: {
dark: '#bebebe',
light: '#a6a5a5'
},
taskPromptCommandTextColor: {
dark: "#e4e4e4",
light: "#000000",
},
taskContextColor: {
dark: "#122848",
light: "#acc0da",
},
taskContextImpersonationColor: {
dark: "#641616",
light: "#dec0c0",
},
taskContextExtraColor: {
dark: "#2a5953",
light: "#a7ce9d",
},
emptyFolderColor: {
dark: '#bebebe',
light: '#a6a5a5'
},
outputBackgroundColor: {
dark: '#282828',
light: '#f6f6f6'
},
outputTextColor: {
dark: '#f6f6f6',
light: '#282828',
},
borderColor: {
dark: "#595858",
light: "#c5c5c5"
}
},
}
export const mePreferences = makeVar(operatorSettingDefaults);
export const successfulLogin = (data) => {
localStorage.setItem("access_token", data.access_token);
localStorage.setItem("refresh_token", data.refresh_token);
localStorage.setItem("user", JSON.stringify(data.user));
let now = new Date();
let serverNow = new Date(data.user.current_utc_time);
const difference = (serverNow.getTime() - now.getTime());
let me = {...data.user};
me.server_skew = difference;
me.login_time = now;
meState({
loggedIn: true,
...data
access_token: data.access_token,
refresh_token: data.refresh_token,
user: {
...me
}
});
localStorage.setItem("user", JSON.stringify(me));
restartWebsockets();
}
export const successfulRefresh = (data) => {
localStorage.setItem("access_token", data.access_token);
localStorage.setItem("refresh_token", data.refresh_token);
let now = new Date();
let serverNow = new Date(data.user.current_utc_time);
const difference = (serverNow.getTime() - now.getTime()) ;
let me = {...meState().user};
me.server_skew = difference;
me.login_time = now;
meState({
loggedIn: true,
access_token: localStorage.getItem("access_token"),
...meState()
access_token: data.access_token,
refresh_token: data.refresh_token,
user: {
...me
}
});
localStorage.setItem("user", JSON.stringify(me));
}
export const FailedRefresh = () =>{
export const FailedRefresh = (restart_websockets) =>{
console.log("failed refresh");
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
@@ -41,7 +220,11 @@ export const FailedRefresh = () =>{
refresh_token: null,
user: null
});
mePreferences(operatorSettingDefaults);
snackActions.clearAll();
restartWebsockets();
if(restart_websockets){
restartWebsockets();
}
}
+378 -128
View File
@@ -12,19 +12,20 @@ import { Home } from './pages/Home/Home';
import { LoggedInRoute } from './utilities/LoggedInRoute';
import { Callbacks } from './pages/Callbacks/Callbacks';
import { Search } from './pages/Search/Search';
import { ConsumingServices } from './pages/ConsumingServices/ConsumingServices';
import React, {createContext} from 'react';
import { TopAppBar } from './TopAppBar';
import { Typography } from '@mui/material';
import { useReactiveVar } from '@apollo/client';
import { useDarkMode } from './utilities/useDarkMode';
import { SingleTaskView } from './pages/SingleTaskView/SingleTaskView';
import { createTheme, ThemeProvider, StyledEngineProvider, adaptV4Theme } from '@mui/material/styles';
import { createTheme, ThemeProvider, StyledEngineProvider } from '@mui/material/styles';
import { GlobalStyles } from '../themes/GlobalStyles';
import CssBaseline from '@mui/material/CssBaseline';
import { FailedRefresh, meState } from '../cache';
import {FailedRefresh, mePreferences, meState, operatorSettingDefaults} from '../cache';
import { Reporting } from './pages/Reporting/Reporting';
import { MitreAttack } from './pages/MITRE_ATTACK/MitreAttack';
import {Tags} from './pages/Tags/Tags';
import { Tooltip } from 'react-tooltip';
import {useLazyQuery, gql } from '@apollo/client';
//background-color: #282c34;
import { Route, Routes } from 'react-router-dom';
import { useInterval } from './utilities/Time';
@@ -35,109 +36,260 @@ import { ToastContainer } from 'react-toastify';
import "react-toastify/dist/ReactToastify.css";
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 {
status
error
preferences
}
}
`;
export function App(props) {
const me = useReactiveVar(meState);
const preferences = useReactiveVar(mePreferences);
const [loadingPreference, setLoadingPreferences] = React.useState(true);
const [themeMode, themeToggler] = useDarkMode();
const localStorageFontSize = localStorage.getItem(`${me?.user?.user_id || 0}-fontSize`);
const initialLocalStorageFontSizeValue = localStorageFontSize === null ? 12 : parseInt(localStorageFontSize);
const localStorageFontFamily = localStorage.getItem(`${me?.user?.user_id || 0}-fontFamily`);
const initialLocalStorageFontFamilyValue = localStorageFontFamily === null ? [
'-apple-system',
'BlinkMacSystemFont',
'"Segoe UI"',
'Roboto',
'"Helvetica Neue"',
'Arial',
'sans-serif',
'"Apple Color Emoji"',
'"Segoe UI Emoji"',
'"Segoe UI Symbol"',
].join(',') : localStorageFontFamily;
const localStorageTopColor = localStorage.getItem(`${me?.user?.user_id || 0}-topColor`);
const initialLocalStorageTopColorValue = localStorageTopColor === null ? "#3c4d67" : localStorageTopColor;
const theme = React.useMemo(
() =>
createTheme(adaptV4Theme({
transitions: {
// So we have `transition: none;` everywhere
create: () => 'none',
},
palette: {
primary: {
main: themeMode === "dark" ? "rgb(70,91,115)" : "rgb(117,133,155)",
() => {
try{
return createTheme({
transitions: {
// So we have `transition: none;` everywhere
create: () => 'none',
},
error: {
main: themeMode === "dark" ? '#da3237' : '#c42c32',
palette: {
contrastThreshold: 4.5,
//tonalOffset: 0.5,
primary: {
main: themeMode === "dark" ? preferences?.palette?.primary?.dark || operatorSettingDefaults.palette.primary.dark :
preferences?.palette?.primary?.light || operatorSettingDefaults.palette.primary.light,
},
error: {
main: themeMode === "dark" ? preferences?.palette?.error?.dark || operatorSettingDefaults.palette.error.dark :
preferences?.palette?.error?.light || operatorSettingDefaults.palette.error.light,
},
success: {
main: themeMode === 'dark' ? preferences?.palette?.success?.dark || operatorSettingDefaults.palette.success.dark :
preferences?.palette?.success?.light || operatorSettingDefaults.palette.success.light,
},
secondary: {
main: themeMode === 'dark' ? preferences?.palette?.secondary?.dark || operatorSettingDefaults.palette.secondary.dark :
preferences?.palette?.secondary?.light || operatorSettingDefaults.palette.secondary.light,
},
info: {
main: themeMode === 'dark' ? preferences?.palette?.info?.dark || operatorSettingDefaults.palette.info.dark :
preferences?.palette?.info?.light || operatorSettingDefaults.palette.info.light,
},
warning: {
main: themeMode === 'dark' ? preferences?.palette?.warning?.dark || operatorSettingDefaults.palette.warning.dark :
preferences?.palette?.warning?.light || operatorSettingDefaults.palette.warning.light,
},
mode: themeMode,
background: {
contrast: themeMode === 'dark' ? preferences?.palette?.background?.light || operatorSettingDefaults.palette.background.light :
preferences?.palette?.background?.dark || operatorSettingDefaults.palette.background.dark,
default: themeMode === "dark" ? preferences?.palette?.background?.dark || operatorSettingDefaults.palette.background.dark :
preferences?.palette?.background?.light || operatorSettingDefaults.palette.background.light,
paper: themeMode === "dark" ? preferences?.palette?.paper?.dark || operatorSettingDefaults.palette.paper.dark :
preferences?.palette?.paper?.light || operatorSettingDefaults.palette.paper.light,
image: themeMode === "dark" ? preferences?.palette?.backgroundImage?.dark || operatorSettingDefaults.palette.backgroundImage.dark :
preferences?.palette?.backgroundImage?.light || operatorSettingDefaults.palette.backgroundImage.light,
},
text: {
primary: themeMode === 'dark' ? preferences?.palette?.text?.dark || operatorSettingDefaults.palette.text.dark :
preferences?.palette?.text?.light || operatorSettingDefaults.palette.text.light,
contrast: themeMode === 'dark' ? '#000' : '#fff',
},
graphGroupRGBA: themeMode === 'dark' ? 'rgba(57, 76, 93, 0.5)' : 'rgba(211, 215, 232, 0.5)',
speedDialAction: themeMode === 'dark' ? '#495054' : '#ffffff',
},
success: {
main: themeMode === 'dark' ? '#44b636' : '#0e7004',
folderColor: '#f1d592',
tableHeader: themeMode === 'dark' ? preferences?.palette?.tableHeader?.dark || operatorSettingDefaults.palette.tableHeader.dark :
preferences?.palette?.tableHeader?.light || operatorSettingDefaults.palette.tableHeader.light,
selectedCallbackColor: themeMode === 'dark' ? preferences?.palette?.selectedCallbackColor?.dark || operatorSettingDefaults.palette.selectedCallbackColor.dark :
preferences?.palette?.selectedCallbackColor?.light || operatorSettingDefaults.palette.selectedCallbackColor.light,
selectedCallbackHierarchyColor: themeMode === 'dark' ? preferences?.palette?.selectedCallbackHierarchyColor?.dark || operatorSettingDefaults.palette.selectedCallbackHierarchyColor.dark :
preferences?.palette?.selectedCallbackHierarchyColor?.light || operatorSettingDefaults.palette.selectedCallbackHierarchyColor.light,
tableHover: themeMode === 'dark' ? preferences?.palette?.tableHover?.dark || operatorSettingDefaults.palette.tableHover.dark :
preferences?.palette?.tableHover?.light || operatorSettingDefaults.palette.tableHover.light,
navBarTextIconColor: themeMode === 'dark' ? preferences?.palette?.navBarIcons?.dark || operatorSettingDefaults.palette.navBarIcons.dark :
preferences?.palette?.navBarIcons?.light || operatorSettingDefaults.palette.navBarIcons.light,
navBarTextColor: themeMode === 'dark' ? preferences?.palette?.navBarText?.dark || operatorSettingDefaults.palette.navBarText.dark :
preferences?.palette?.navBarText?.light || operatorSettingDefaults.palette.navBarText.light,
pageHeader: {
main: themeMode === 'dark' ? preferences?.palette?.pageHeader?.dark || operatorSettingDefaults.palette.pageHeader.dark :
preferences?.palette?.pageHeader?.light || operatorSettingDefaults.palette.pageHeader.light,
},
secondary: {
main: themeMode === 'dark' ? '#bebebe' : '#a6a5a5',
pageHeaderText: {
main: 'white',
},
info: {
main: themeMode === 'dark' ? '#2574b4' : '#4990b2',
topAppBarColor: themeMode === 'dark' ? (preferences?.palette?.navBarColor?.dark || operatorSettingDefaults.palette.navBarColor.dark) :
(preferences?.palette?.navBarColor?.light || operatorSettingDefaults.palette.navBarColor.light),
topAppBarBottomColor: themeMode === 'dark' ? preferences?.palette?.navBarBottomColor?.dark || operatorSettingDefaults.palette.navBarBottomColor.dark :
preferences?.palette?.navBarBottomColor?.light || operatorSettingDefaults.palette.navBarBottomColor.light,
typography: {
fontSize: 12, //preferences?.fontSize,
fontFamily: preferences?.fontFamily
},
mode: themeMode,
background: {
contrast: themeMode === 'dark' ? '#e1e0e0' : 'rgb(44, 52, 60)',
default: themeMode === "dark" ? 'rgb(48, 48, 48)' : '#f6f6f6',
paper: themeMode === "dark" ? 'rgb(37,36,36)' : '#ececec',
taskLabel: themeMode === "dark" ? 'rgb(20, 20, 20)' : '#f5f5f5',
taskPromptTextColor: themeMode === 'dark' ? preferences?.palette?.taskPromptTextColor?.dark || operatorSettingDefaults.palette.taskPromptTextColor.dark :
preferences?.palette?.taskPromptTextColor?.light || operatorSettingDefaults.palette.taskPromptTextColor.light,
taskPromptCommandTextColor: themeMode === 'dark' ? preferences?.palette?.taskPromptCommandTextColor?.dark || operatorSettingDefaults.palette.taskPromptCommandTextColor.dark :
preferences?.palette?.taskPromptCommandTextColor?.light || operatorSettingDefaults.palette.taskPromptCommandTextColor.light,
taskContextColor: themeMode === 'dark' ? preferences?.palette?.taskContextColor?.dark || operatorSettingDefaults.palette.taskContextColor.dark :
preferences?.palette?.taskContextColor?.light || operatorSettingDefaults.palette.taskContextColor.light,
taskContextImpersonationColor: themeMode === 'dark' ? preferences?.palette?.taskContextImpersonationColor?.dark || operatorSettingDefaults.palette.taskContextImpersonationColor.dark :
preferences?.palette?.taskContextImpersonationColor?.light || operatorSettingDefaults.palette.taskContextImpersonationColor.light,
taskContextExtraColor: themeMode === 'dark' ? preferences?.palette?.taskContextExtraColor?.dark || operatorSettingDefaults.palette.taskContextExtraColor.dark :
preferences?.palette?.taskContextExtraColor?.light || operatorSettingDefaults.palette.taskContextExtraColor.light,
emptyFolderColor: themeMode === 'dark' ? preferences?.palette?.emptyFolderColor?.dark || operatorSettingDefaults.palette.emptyFolderColor.dark :
preferences?.palette?.emptyFolderColor?.light || operatorSettingDefaults.palette.emptyFolderColor.light,
outputBackgroundColor: themeMode === 'dark' ? preferences?.palette?.outputBackgroundColor?.dark || operatorSettingDefaults.palette.outputBackgroundColor.dark :
preferences?.palette?.outputBackgroundColor?.light || operatorSettingDefaults.palette.outputBackgroundColor.light,
outputTextColor: themeMode === 'dark' ? preferences?.palette?.outputTextColor?.dark || operatorSettingDefaults.palette.outputTextColor.dark :
preferences?.palette?.outputTextColor?.light || operatorSettingDefaults.palette.outputTextColor.light,
borderColor: themeMode === 'dark' ? preferences?.palette?.borderColor?.dark || operatorSettingDefaults.palette.borderColor.dark :
preferences?.palette?.borderColor?.light || operatorSettingDefaults.palette.borderColor.light,
})
}catch(error){
console.log(error);
snackActions.error(error.message);
return createTheme({
transitions: {
// So we have `transition: none;` everywhere
create: () => 'none',
},
listSubHeader: {
default: themeMode === "dark" ? 'rgb(50, 50, 50)' : 'rgb(240, 240, 240)',
palette: {
contrastThreshold: 4.5,
//tonalOffset: 0.5,
primary: {
main: themeMode === "dark" ? operatorSettingDefaults.palette.primary.dark :
operatorSettingDefaults.palette.primary.light,
},
error: {
main: themeMode === "dark" ? operatorSettingDefaults.palette.error.dark :
operatorSettingDefaults.palette.error.light,
},
success: {
main: themeMode === 'dark' ? operatorSettingDefaults.palette.success.dark :
operatorSettingDefaults.palette.success.light,
},
secondary: {
main: themeMode === 'dark' ? operatorSettingDefaults.palette.secondary.dark :
operatorSettingDefaults.palette.secondary.light,
},
info: {
main: themeMode === 'dark' ? operatorSettingDefaults.palette.info.dark :
operatorSettingDefaults.palette.info.light,
},
warning: {
main: themeMode === 'dark' ? operatorSettingDefaults.palette.warning.dark :
operatorSettingDefaults.palette.warning.light,
},
mode: themeMode,
background: {
contrast: themeMode === 'dark' ?operatorSettingDefaults.palette.background.light :
operatorSettingDefaults.palette.background.dark,
default: themeMode === "dark" ? operatorSettingDefaults.palette.background.dark :
operatorSettingDefaults.palette.background.light,
paper: themeMode === "dark" ? operatorSettingDefaults.palette.paper.dark :
operatorSettingDefaults.palette.paper.light,
image: themeMode === "dark" ? operatorSettingDefaults.palette.backgroundImage.dark :
operatorSettingDefaults.palette.backgroundImage.light,
},
text: {
primary: themeMode === 'dark' ? operatorSettingDefaults.palette.text.dark :
operatorSettingDefaults.palette.text.light,
contrast: themeMode === 'dark' ? '#000' : '#fff',
},
graphGroupRGBA: themeMode === 'dark' ? 'rgba(57, 76, 93, 0.5)' : 'rgba(211, 215, 232, 0.5)',
speedDialAction: themeMode === 'dark' ? '#495054' : '#ffffff',
},
text: {
contrast: themeMode === 'dark' ? '#000' : '#fff',
folderColor: '#f1d592',
tableHeader: themeMode === 'dark' ? operatorSettingDefaults.palette.tableHeader.dark :
operatorSettingDefaults.palette.tableHeader.light,
selectedCallbackColor: themeMode === 'dark' ? operatorSettingDefaults.palette.selectedCallbackColor.dark :
operatorSettingDefaults.palette.selectedCallbackColor.light,
selectedCallbackHierarchyColor: themeMode === 'dark' ? operatorSettingDefaults.palette.selectedCallbackHierarchyColor.dark :
operatorSettingDefaults.palette.selectedCallbackHierarchyColor.light,
tableHover: themeMode === 'dark' ? operatorSettingDefaults.palette.tableHover.dark :
operatorSettingDefaults.palette.tableHover.light,
navBarTextIconColor: themeMode === 'dark' ? operatorSettingDefaults.palette.navBarIcons.dark :
operatorSettingDefaults.palette.navBarIcons.light,
navBarTextColor: themeMode === 'dark' ? operatorSettingDefaults.palette.navBarText.dark :
operatorSettingDefaults.palette.navBarText.light,
pageHeader: {
main: themeMode === 'dark' ? operatorSettingDefaults.palette.pageHeader.dark :
operatorSettingDefaults.palette.pageHeader.light,
},
textBackgroundColor: themeMode === 'dark' ? '#272c2f' : '#e9eaea',
textBackgroundColorPrimary: themeMode === 'dark' ? '#436b9f' : '#aadcf5',
textBackgroundColorSuccess: themeMode === 'dark' ? '#09a21a' : '#70e373',
textBackgroundColorError: themeMode === 'dark' ? '#9f1616' : '#f19da3',
graphGroup: themeMode === 'dark' ? '#394c5d' : '#d3d7e8',
graphGroupRGBA: themeMode === 'dark' ? 'rgba(57, 76, 93, 0.5)' : 'rgba(211, 215, 232, 0.5)',
speedDialAction: themeMode === 'dark' ? '#495054' : '#ffffff',
},
pageHeaderTextColor: "#ffffff",
folderColor: '#f1d592',
tableHeader: themeMode === 'dark' ? '#484848' : '#c4c4c4',
successOnMain: '#1ae302',
errorOnMain: '#ff656b',
infoOnMain: '#67ceff',
selectedCallbackColor: themeMode === 'dark' ? '#26456e' : '#c6e5f6',
selectedCallbackHierarchyColor: themeMode === 'dark' ? '#273e5d' : '#deeff8',
materialReactTableHeader: themeMode === 'dark' ? '#484848' : '#d5d5d5',
tableBorder: themeMode === 'dark' ? 'rgba(81,81,81,1)' : 'rgba(224,224,224,1)',
tableHover: themeMode === 'dark' ? 'rgba(60,60,60)' : 'rgb(232,232,232)',
pageHeader: {
main: '#706c6e',
},
pageHeaderSecondary: {
main: '#444343',
},
pageHeaderText: {
main: 'white',
},
topAppBarColor: initialLocalStorageTopColorValue,
typography: {
fontSize: initialLocalStorageFontSizeValue,
fontFamily: initialLocalStorageFontFamilyValue
},
})),
[themeMode]
pageHeaderText: {
main: 'white',
},
topAppBarColor: themeMode === 'dark' ? operatorSettingDefaults.palette.navBarColor.dark :
operatorSettingDefaults.palette.navBarColor.light,
topAppBarBottomColor: themeMode === 'dark' ? operatorSettingDefaults.palette.navBarBottomColor.dark :
operatorSettingDefaults.palette.navBarBottomColor.light,
emptyFolderColor: themeMode === 'dark' ? operatorSettingDefaults.palette.emptyFolderColor.dark :
operatorSettingDefaults.palette.emptyFolderColor.light,
outputBackgroundColor: themeMode === 'dark' ? operatorSettingDefaults.palette.outputBackgroundColor.dark :
operatorSettingDefaults.palette.outputBackgroundColor.light,
outputTextColor: themeMode === 'dark' ? operatorSettingDefaults.palette.outputTextColor.dark :
operatorSettingDefaults.palette.outputTextColor.light,
borderColor: themeMode === 'dark' ? operatorSettingDefaults.palette.borderColor.dark :
operatorSettingDefaults.palette.borderColor.light,
typography: {
fontSize: 12,//operatorSettingDefaults?.fontSize,
fontFamily: operatorSettingDefaults?.fontFamily
},
})
}
},[themeMode, loadingPreference, preferences.fontSize, preferences.fontFamily, preferences.palette]
);
const mountedRef = React.useRef(true);
const [openRefreshDialog, setOpenRefreshDialog] = React.useState(false);
const [getUserPreferences] = useLazyQuery(userSettingsQuery, {
fetchPolicy: "no-cache",
onCompleted: (data) => {
//console.log("got preferences", data.getOperatorPreferences.preferences)
if(data.getOperatorPreferences.status === "success"){
if(data.getOperatorPreferences.preferences !== null){
mePreferences({...preferences, ...data.getOperatorPreferences.preferences});
}
} else {
snackActions.error(`Failed to get user preferences:\n${data.getOperatorPreferences.error}`);
}
setLoadingPreferences(false);
},
onError: (error) => {
console.log(error);
snackActions.error(error.message);
setLoadingPreferences(false);
}
})
useInterval( () => {
// interval should run every 10 minutes (600000 milliseconds) to check JWT status
let millisecondsLeft = JWTTimeLeft();
// if we have 30min left of our token, prompt the user to extend. 30 min is 1,800,000 milliseconds
//console.log("jwt time left: ", millisecondsLeft)
if(millisecondsLeft <= 1800000 && !openRefreshDialog){
if(millisecondsLeft <= 1800000 && !openRefreshDialog && me.loggedIn){
if(isJWTValid()){
setOpenRefreshDialog(true);
}else{
@@ -145,59 +297,157 @@ export function App(props) {
}
}
}, 600000, mountedRef, mountedRef);
React.useEffect( () => {
if(me.loggedIn){
setLoadingPreferences(true);
getUserPreferences();
} else {
setLoadingPreferences(false);
}
}, [me.loggedIn])
if(loadingPreference){
// make sure we've loaded preferences before loading actual app content
return (
<StyledEngineProvider injectFirst>
<ThemeProvider theme={theme}>
<GlobalStyles theme={theme} />
<CssBaseline />
</ThemeProvider>
</StyledEngineProvider>
)
}
const background = theme.palette.background.image !== null ? {
backgroundImage: "linear-gradient(" + theme.palette.background.default + "99" + "," + theme.palette.background.default + "99" + ")," + theme.palette.background.image ,
backgroundRepeat: "no-repeat",
backgroundSize: "100% 100%"
} : {
backgroundColor: theme.palette.background.default
};
return (
<StyledEngineProvider injectFirst>
<ThemeProvider theme={theme}>
<GlobalStyles theme={theme} />
<CssBaseline />
<ToastContainer limit={2} autoClose={3000}
theme={themeMode}
style={{maxWidth: "100%", minWidth: "40%", width: "40%", marginTop: "20px", display: "flex", flexWrap: "wrap",
wordBreak: "break-all", flexDirection: "column", justifyContent: "center"}}
pauseOnFocusLoss={false} />
<div style={{ maxHeight: '100%', height: '100%', display: 'flex', flexDirection: 'column' }}>
<div style={{ minHeight: '50px', flexGrow: 0 }}>
{me.loggedIn && me.user !== undefined && me.user !== null ? (
<TopAppBar me={me} theme={themeMode} toggleTheme={themeToggler} />
) : null}
</div>
{openRefreshDialog &&
<MythicDialog fullWidth={true} maxWidth="sm" open={openRefreshDialog}
onClose={()=>{setOpenRefreshDialog(false);}}
innerDialog={<RefreshTokenDialog
onClose={()=>{setOpenRefreshDialog(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);}}
innerDialog={<RefreshTokenDialog
onClose={()=>{setOpenRefreshDialog(false);}} />}
/>
}
<div style={{ margin: '0px 2px 0px 5px', flexGrow: 1, flexDirection: 'column', height: "calc(100% - 5rem)", }}>
<Routes>
<Route path='/new/login' element={<LoginForm me={me}/>}/>
<Route path='/new/invite' element={<InviteForm me={me}/>}/>
<Route path='/' element={<LoggedInRoute me={me}><Home me={me}/></LoggedInRoute>} />
<Route exact path='/new' element={<LoggedInRoute me={me}><Home me={me}/></LoggedInRoute>} />
<Route exact path='/new/settings' element={<LoggedInRoute me={me}><Settings me={me}/></LoggedInRoute>} />
<Route exact path='/new/payloadtypes' element={<LoggedInRoute me={me}><PayloadTypesC2Profiles me={me}/></LoggedInRoute>} />
<Route exact path='/new/eventfeed' element={<LoggedInRoute me={me}><EventFeed me={me}/></LoggedInRoute>} />
<Route exact path='/new/createpayload' element={<LoggedInRoute me={me}><CreatePayload me={me}/></LoggedInRoute>} />
<Route exact path='/new/createwrapper' element={<LoggedInRoute me={me}><CreatePayloadWrapper me={me}/></LoggedInRoute>} />
<Route exact path='/new/payloads' element={<LoggedInRoute me={me}><Payloads me={me} /></LoggedInRoute>} />
<Route exact path='/new/c2profiles' element={<LoggedInRoute me={me}><PayloadTypesC2Profiles me={me}/></LoggedInRoute>} />
<Route exact path='/new/services/' element={<LoggedInRoute me={me}><PayloadTypesC2Profiles me={me}/></LoggedInRoute>} />
<Route exact path='/new/callbacks' element={<LoggedInRoute me={me}><Callbacks me={me}/></LoggedInRoute>} />
<Route path='/new/search' element={<LoggedInRoute me={me}><Search history={props.history} me={me} /></LoggedInRoute>} />
<Route exact path='/new/browserscripts' element={<LoggedInRoute me={me}><BrowserScripts me={me}/></LoggedInRoute>} />
<Route exact path='/new/task/:taskId' element={<LoggedInRoute me={me}><SingleTaskView me={me}/></LoggedInRoute>} />
<Route exact path='/new/tasks/by_range' element={<LoggedInRoute me={me}><SingleTaskView me={me}/></LoggedInRoute>} />
<Route exact path='/new/operations' element={<LoggedInRoute me={me}><Operations me={me}/></LoggedInRoute>} />
<Route exact path='/new/callbacks/:callbackDisplayId' element={<LoggedInRoute me={me}><ExpandedCallback me={me}/></LoggedInRoute>} />
<Route exact path='/new/reporting' element={<LoggedInRoute me={me}><Reporting me={me}/></LoggedInRoute>} />
<Route exact path='/new/mitre' element={<LoggedInRoute me={me}><MitreAttack me={me}/></LoggedInRoute>} />
<Route exact path='/new/tagtypes' element={<LoggedInRoute me={me}><Tags me={me}/></LoggedInRoute>} />
<Route exact path='/new/consuming_services' element={<LoggedInRoute me={me}><ConsumingServices me={me}/></LoggedInRoute>} />
<Route exact path='/new/eventing' element={<LoggedInRoute me={me}><Eventing me={me}/></LoggedInRoute>} />
</Routes>
{me.loggedIn && me.user !== undefined && me.user !== null &&
<TopAppBarVertical me={me} toggleTheme={themeToggler} />
}
<div style={{
maxHeight: '100%',
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
overflow: "hidden",
}}>
<div style={{height: "5px", width: "100%", background: me.loggedIn ? `linear-gradient(25deg, ${theme.topAppBarColor}, ${theme.topAppBarBottomColor})` : ""}}/>
{me.loggedIn && me?.user?.current_operation_banner_text !== "" &&
<Typography style={{
backgroundColor: me?.user?.current_operation_banner_color,
width: "100%",
textAlign: "center",
fontWeight: "600",
color: "white",
border: `1px solid ${theme.topAppBarColor || "grey"}`
}}>
{me?.user?.current_operation_banner_text}
</Typography>
}
{me.loggedIn && me?.badConnection
&&
<Typography style={{
backgroundColor: theme.palette.error.main,
width: "100%",
textAlign: "center",
fontWeight: "600",
color: "white",
border: `1px solid ${theme.topAppBarColor || "grey"}`
}}>
{"Can't connect to Mythic. Please check connection and refresh"}
</Typography>
}
<div style={{
margin: '0px 0px 0px 0px',
flexGrow: 1,
display: "flex",
flexDirection: 'column',
overflow:"hidden"
}}>
<Routes>
<Route path='/new/login' element={<LoginForm me={me}/>}/>
<Route path='/new/invite' element={<InviteForm me={me}/>}/>
<Route path='/' element={<LoggedInRoute me={me}><Home me={me}/></LoggedInRoute>}/>
<Route exact path='/new'
element={<LoggedInRoute me={me}><Home me={me}/></LoggedInRoute>}/>
<Route exact path='/new/settings'
element={<LoggedInRoute me={me}><Settings me={me}/></LoggedInRoute>}/>
<Route exact path='/new/payloadtypes'
element={<LoggedInRoute me={me}><PayloadTypesC2Profiles
me={me}/></LoggedInRoute>}/>
<Route exact path='/new/eventfeed'
element={<LoggedInRoute me={me}><EventFeed me={me}/></LoggedInRoute>}/>
<Route exact path='/new/createpayload'
element={<LoggedInRoute me={me}><CreatePayload me={me}/></LoggedInRoute>}/>
<Route exact path='/new/createwrapper'
element={<LoggedInRoute me={me}><CreatePayloadWrapper
me={me}/></LoggedInRoute>}/>
<Route exact path='/new/payloads'
element={<LoggedInRoute me={me}><Payloads me={me}/></LoggedInRoute>}/>
<Route exact path='/new/c2profiles'
element={<LoggedInRoute me={me}><PayloadTypesC2Profiles
me={me}/></LoggedInRoute>}/>
<Route exact path='/new/services/'
element={<LoggedInRoute me={me}><PayloadTypesC2Profiles
me={me}/></LoggedInRoute>}/>
<Route exact path='/new/callbacks'
element={<LoggedInRoute me={me}><Callbacks me={me}/></LoggedInRoute>}/>
<Route path='/new/search'
element={<LoggedInRoute me={me}><Search history={props.history}
me={me}/></LoggedInRoute>}/>
<Route exact path='/new/browserscripts'
element={<LoggedInRoute me={me}><BrowserScripts me={me}/></LoggedInRoute>}/>
<Route exact path='/new/task/:taskId'
element={<LoggedInRoute me={me}><SingleTaskView me={me}/></LoggedInRoute>}/>
<Route exact path='/new/tasks/by_range'
element={<LoggedInRoute me={me}><SingleTaskView me={me}/></LoggedInRoute>}/>
<Route exact path='/new/operations'
element={<LoggedInRoute me={me}><Operations me={me}/></LoggedInRoute>}/>
<Route exact path='/new/callbacks/:callbackDisplayId'
element={<LoggedInRoute me={me}><ExpandedCallback
me={me}/></LoggedInRoute>}/>
<Route exact path='/new/reporting'
element={<LoggedInRoute me={me}><Reporting me={me}/></LoggedInRoute>}/>
<Route exact path='/new/mitre'
element={<LoggedInRoute me={me}><MitreAttack me={me}/></LoggedInRoute>}/>
<Route exact path='/new/tagtypes'
element={<LoggedInRoute me={me}><Tags me={me}/></LoggedInRoute>}/>
<Route exact path='/new/eventing'
element={<LoggedInRoute me={me}><Eventing me={me}/></LoggedInRoute>}/>
<Route exact path='/new/jupyter' element={<LoggedInRoute me={me}><Jupyter/></LoggedInRoute>}/>
<Route exact path='/new/hasura' element={<LoggedInRoute me={me}><Hasura/></LoggedInRoute>}/>
</Routes>
</div>
</div>
</div>
</MeContext.Provider>
</ThemeProvider>
</StyledEngineProvider>
);
@@ -1,6 +1,7 @@
import React, {useEffect} from 'react';
import {gql, useSubscription} from '@apollo/client';
import {snackActions} from './utilities/Snackbar';
import {getSkewedNow} from "./utilities/Time";
//fromNow must be in ISO format for hasura/postgres stuff
//new Date().toISOString() will do it
@@ -16,54 +17,53 @@ const subscribe_payloads = gql`
level
resolved
source
warning
}
}
`;
export function EventFeedNotifications(props) {
const me = props.me;
const [fromNow, setFromNow] = React.useState(getSkewedNow().toISOString());
//const fromNow = React.useRef( );
const { loading, error, data } = useSubscription(subscribe_payloads, {
variables: {fromNow: (new Date()).toISOString()},
useSubscription(subscribe_payloads, {
variables: {fromNow: fromNow },
fetchPolicy: "no-cache",
shouldResubscribe: true,
onError: (errorData) => {
snackActions.warning("Failed to get event notifications");
console.log(errorData);
},
onData: ({data}) => {
if (data.data.operationeventlog_stream.length > 0){
if(data.data.operationeventlog_stream[0].level === "api" ||
data.data.operationeventlog_stream[0].level === "debug" ||
data.data.operationeventlog_stream[0].level === "agent"
){
return;
}
if(data.data.operationeventlog_stream[0].resolved){
return;
}
if(data.data.operationeventlog_stream[0].operator){
const message = data.data.operationeventlog_stream[0].operator.username + ":\n" + data.data.operationeventlog_stream[0].message;
if (data.data.operationeventlog_stream[0].warning) {
snackActions.warning(message, { autoClose: 2000});
} else {
snackActions.info(message, { autoClose: 2000});
}
}else {
if (data.data.operationeventlog_stream[0].warning) {
snackActions.warning(data.data.operationeventlog_stream[0].message, {autoClose: 3000});
} else {
snackActions.info(
<div style={{width: "100%"}}>{data.data.operationeventlog_stream[0].message}</div>,
{autoClose: 3000});
}
}
}
}
});
useEffect( () => {
//console.log(data, loading, error, fromNow.current);
if(!loading && !error && data && data.operationeventlog_stream.length > 0){
if(data.operationeventlog_stream[0].source === "debug" || data.operationeventlog_stream[0].level === "debug" ){
return;
}
if(data.operationeventlog_stream[0].resolved){
return;
}
if(data.operationeventlog_stream[0].operator){
const message = data.operationeventlog_stream[0].operator.username + ":\n" + data.operationeventlog_stream[0].message;
if (data.operationeventlog_stream[0].level === "warning") {
snackActions.warning(message, { autoClose: 2000});
} else {
snackActions.info(message, { autoClose: 2000});
}
}else {
if (data.operationeventlog_stream[0].level === "warning") {
snackActions.warning(data.operationeventlog_stream[0].message, {autoClose: 3000});
} else {
snackActions.info(
<div style={{width: "100%"}}>{data.operationeventlog_stream[0].message}</div>,
{autoClose: 3000});
}
}
}else if(error){
console.error(error);
snackActions.error("Mythic encountered an error getting operational event stream", {autoHideDuration: 2000});
}
}, [loading, data, error, me.user]);
return null;
return null
}
@@ -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,38 +49,103 @@ 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"
onClick={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);
if(props.dontCloseOnSubmit){
return;
}
props.onClose();
}
const onChange = (value) => {
@@ -77,34 +161,48 @@ export function MythicModifyStringDialog(props) {
}, [props.value]);
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">{props.title}</DialogTitle>
<DialogContent dividers={true} style={{height: "100%", margin: 0, padding: 0}}>
{props.title !== "" &&
<MythicDraggableDialogTitle>{props.title}
<MythicStyledTooltip title={wrap ? "Toggle off word wrap" : "Toggl on word wrap"}
tooltipStyle={{float: "right"}}>
<IconButton onClick={() => {setWrap(!wrap)}}>
<WrapTextIcon color={wrap ? "success" : "secondary"} />
</IconButton>
</MythicStyledTooltip>
</MythicDraggableDialogTitle>
}
<DialogContent dividers={true} style={{ margin: 0, padding: 0}}>
<AceEditor
mode="json"
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
width="100%"
height="100%"
wrapEnabled={props.wrap ? props.wrap : false}
minLines={props.maxRows ? props.maxRows : 10}
maxLines={props.maxRows ? props.maxRows : 10}
fontSize={14}
showPrintMargin={false}
wrapEnabled={wrap}
value={comment}
focus={true}
onChange={onChange}
setOptions={{
useWorker: false
tabSize: 4,
useWorker: false,
showInvisibles: false,
}}
/>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
Close
</Button>
{props.onSubmit &&
<Button onClick={onCommitSubmit} variant="contained" color="success">
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>
);
}
@@ -123,7 +221,7 @@ export function MythicViewJSONAsTableDialog(props) {
permissionDict = JSON.parse(props.value);
}
if(permissionDict.constructor === Object){
if(!Array.isArray(permissionDict) && typeof permissionDict !== 'string'){
for(let key in permissionDict){
if(permissionDict[key] && permissionDict[key].constructor === Object){
// potentially have a nested dictionary here or array to become a dictionary, mark it
@@ -176,9 +274,9 @@ 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">
<TableContainer className="mythicElement" style={{paddingLeft: "10px"}}>
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
<TableHead>
<TableRow>
@@ -1,26 +1,16 @@
import React from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import {MythicDialog} from './MythicDialog';
import {MythicDialog, MythicModifyStringDialog} from './MythicDialog';
export function MythicDisplayTextDialog(props) {
return (
<MythicDialog fullWidth={props.fullWidth === null ? false : props.fullWidth} maxWidth={props.maxWidth === null ? "sm" : props.maxWidth} open={props.open} onClose={()=>{props.onClose()}} innerDialog={
<React.Fragment>
<DialogTitle id="form-dialog-title">{props.title}</DialogTitle>
<DialogContent dividers={true}>
<pre style={{whiteSpace: "pre-wrap"}}>
{props.value}
</pre>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
Close
</Button>
</DialogActions>
</React.Fragment>
<MythicDialog fullWidth={props.fullWidth === null ? false : props.fullWidth}
maxWidth={props.maxWidth === null ? "sm" : props.maxWidth}
open={props.open}
onClose={()=>{props.onClose()}} innerDialog={
<MythicModifyStringDialog title={props.title}
onClose={props.onClose}
value={props.value}
maxRows={20} />
} />
);
}
@@ -0,0 +1,10 @@
import DialogTitle from '@mui/material/DialogTitle';
export const MythicDraggableDialogTitle = ({children}) => {
return (
<DialogTitle id="mythic-draggable-title" style={{ cursor: 'move', width: "100%" }}>
{children}
</DialogTitle>
)
}
@@ -0,0 +1,11 @@
export const reorder = (
list,
startIndex,
endIndex
) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
@@ -239,7 +239,7 @@ const VirtualTreeRow = React.memo(({
height: "15px",
marginLeft: '3px',
marginRight: '5px',
color: item?.metadata?.has_children ? theme.folderColor : theme.palette.text.secondary,
color: item?.has_children || item.success ? theme.folderColor : theme.emptyFolderColor,
}}
size={"lg"}
onClick={handleOnClickButton} />
@@ -250,14 +250,14 @@ const VirtualTreeRow = React.memo(({
height: "15px",
marginLeft: '3px',
marginRight: '5px',
color: item?.metadata?.has_children ? theme.folderColor : theme.palette.text.secondary, }}
color: item?.has_children || item.success ? theme.folderColor : theme.emptyFolderColor, }}
size={"lg"}
icon={faFolder} onClick={handleOnClickButton} />
)}
<Typography
style={{
color:
item?.metadata?.has_children ||
item?.has_children ||
item.success !== null
? theme.palette.text.primary
: theme.palette.text.secondary,
@@ -439,7 +439,7 @@ const FileBrowserVirtualTreePreMemo = ({
{(AutoSizerProps) => (
<List
itemData={flattenedNodes}
direction="vertical"
layout="vertical"
height={AutoSizerProps.height}
width={AutoSizerProps.width}
itemCount={flattenedNodes.length}
@@ -8,16 +8,18 @@ import {b64DecodeUnicode} from "../pages/Callbacks/ResponseDisplay";
import {PreviewFileMediaDialog} from "../pages/Search/PreviewFileMedia";
import {faPhotoVideo} from '@fortawesome/free-solid-svg-icons';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {useMythicLazyQuery} from "../utilities/useMythicLazyQuery";
export const getfileInformationQuery = gql`
query getFileInformation($file_id: String!){
filemeta(where: {agent_file_id: {_eq: $file_id}}){
filename_text
agent_file_id
}
}
`;
export const MythicFileContext = ({agent_file_id, display_link, filename}) => {
export const MythicFileContext = ({agent_file_id, display_link, filename, extraStyles}) => {
const [fileData, setFileData] = React.useState({
agent_file_id: agent_file_id,
display_link: display_link,
@@ -31,30 +33,56 @@ export const MythicFileContext = ({agent_file_id, display_link, filename}) => {
}
setOpenPreviewMediaDialog(true);
}
const [getFileInformation] = useLazyQuery(getfileInformationQuery, {
onCompleted: (data) => {
setFileData( {...fileData, filename: b64DecodeUnicode(data.filemeta[0].filename_text)});
if(display_link === "" || display_link === undefined){
setFileData( {...fileData,
filename: b64DecodeUnicode(data.filemeta[0].filename_text),
display_link: b64DecodeUnicode(data.filemeta[0].filename_text)});
}
},
onError: (data) => {
snackActions.error("Failed to fetch instance data: " + data);
console.log(data);
},
const getFileInformationSuccess = (data) => {
if(data.filemeta.length === 0){
setFileData({
filename: undefined,
display_link: "",
agent_file_id: agent_file_id,
})
return;
}
setFileData( {...fileData,
agent_file_id: data.filemeta[0].agent_file_id,
filename: b64DecodeUnicode(data.filemeta[0].filename_text)});
if(display_link === "" || display_link === undefined){
setFileData( {...fileData,
agent_file_id: data.filemeta[0].agent_file_id,
filename: b64DecodeUnicode(data.filemeta[0].filename_text),
display_link: b64DecodeUnicode(data.filemeta[0].filename_text)});
}
}
const getFileInformationError = (data) => {
snackActions.error("Failed to fetch file data: " + data);
console.log(data);
}
const getFileInformation = useMythicLazyQuery(getfileInformationQuery, {
fetchPolicy: "no-cache"
})
React.useEffect( () => {
if(filename === "" || filename === undefined){
getFileInformation({variables: {file_id: fileData.agent_file_id}})
if(agent_file_id !== '' && agent_file_id !== undefined){
getFileInformation({variables: {file_id: agent_file_id}})
.then(({data}) => getFileInformationSuccess(data)).catch((data) => getFileInformationError(data));
}
}, [filename]);
}, [filename, agent_file_id]);
if(agent_file_id === '' || agent_file_id === undefined){
return null
}
if(fileData.agent_file_id === '' || fileData.filename === undefined){
return agent_file_id
}
const onClose = (e) => {
if(e){
e.preventDefault();
e.stopPropagation();
}
setOpenPreviewMediaDialog(false)
}
return (
<>
<MythicStyledTooltip title={"Preview Media"}>
<FontAwesomeIcon icon={faPhotoVideo} style={{height: "25px", bottom: "5px", position: "relative", cursor: "pointer", display: "inline-block"}}
<MythicStyledTooltip title={"Preview Media"} tooltipStyle={extraStyles ? extraStyles : {}}>
<FontAwesomeIcon icon={faPhotoVideo}
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}>
@@ -62,11 +90,11 @@ export const MythicFileContext = ({agent_file_id, display_link, filename}) => {
</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} />}
/>
}
</>
@@ -14,8 +14,10 @@ export const Dropdown = React.forwardRef(
minWidth,
absoluteX,
absoluteY,
anchorReference,
style
anchorReference="anchorEl",
style,
transformOrigin,
anchorOrigin,
},
ref
) => {
@@ -27,7 +29,6 @@ export const Dropdown = React.forwardRef(
if (ref) {
anchorRef = ref;
}
const handleClose = (event) => {
event.stopPropagation();
@@ -49,7 +50,6 @@ export const Dropdown = React.forwardRef(
parentMenuOpen: isOpen
};
}
return React.createElement(menuItem.type, {
...props,
key: index,
@@ -65,22 +65,24 @@ export const Dropdown = React.forwardRef(
: props.children
});
};
const anchorEl = isOpen && isOpen.currentTarget ? isOpen.currentTarget : isOpen;
return (
<>
<Menu
elevation={5}
PaperProps={{ sx: { minWidth: minWidth ?? 0 } }}
style={{zIndex: 100000}}
anchorEl={isOpen}
style={{zIndex: 100000, position: "absolute"}}
anchorEl={anchorReference === "anchorEl" ? anchorEl : undefined}
transition={"true"}
open={!!externallyOpen}
anchorPosition={anchorReference === "anchorEl" ? undefined : {top: absoluteY, left: absoluteX}}
anchorReference={ anchorReference ? anchorReference : "anchorEl"}
onClose={handleClose}
anchorOrigin={{
anchorOrigin={anchorOrigin ? anchorOrigin : {
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
transformOrigin={transformOrigin ? transformOrigin : {
vertical: 'top',
horizontal: 'left',
}}
@@ -215,6 +217,9 @@ const NestedMenuItem = React.forwardRef((props, ref) => {
</MenuItem>
<Menu
hideBackdrop
disablePortal
disableEnforceFocus={false}
disableAutoFocus={false}
style={{ pointerEvents: "none", zIndex: 100000}}
anchorEl={menuItemRef.current}
anchorOrigin={{
@@ -227,12 +232,13 @@ const NestedMenuItem = React.forwardRef((props, ref) => {
}}
css={customTheme}
open={!!open}
autoFocus={false}
disableAutoFocus
disableEnforceFocus
onClose={() => {
setIsSubMenuOpen(false);
}}
MenuListProps={{
'aria-hidden': false,
role: 'menu'
}}
>
<div ref={menuContainerRef} style={{ pointerEvents: "auto" }}>
{children}
@@ -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>
)
}
@@ -3,9 +3,10 @@ import {classes} from './styles';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import {Dropdown, DropdownMenuItem, DropdownNestedMenuItem} from "../MythicNestedMenus";
const CellPreMemo = ({ VariableSizeGridProps: { style, rowIndex, columnIndex, data } }) => {
const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
const [openContextMenu, setOpenContextMenu] = React.useState(false);
const rowClassName = data.gridUUID + "row" + rowIndex;
const rowHighlight = rowIndex % 2 === 1 ? 'MythicResizableGridRowHighlight' : '';
const [contextMenuOptions, setContextMenuOptions] = React.useState(data?.rowContextMenuOptions || []);
const dropdownAnchorRef = React.useRef(null);
const item = data.items[rowIndex][columnIndex];
@@ -21,6 +22,8 @@ const CellPreMemo = ({ VariableSizeGridProps: { style, rowIndex, columnIndex, da
[data, rowIndex]
);
const handleClick = (event) => {
event.preventDefault();
event.stopPropagation();
if(data.onRowClick){
data.onRowClick({event, rowDataStatic: data.items[rowIndex][columnIndex]?.props?.rowData})
}
@@ -43,6 +46,8 @@ const CellPreMemo = ({ VariableSizeGridProps: { style, rowIndex, columnIndex, da
}
}
const handleMenuItemClick = (event, clickOption) => {
event.preventDefault();
event.stopPropagation();
clickOption({event, columnIndex, rowIndex, data: data.items[rowIndex][columnIndex]?.props?.rowData || {}});
setOpenContextMenu(false);
};
@@ -54,28 +59,31 @@ const CellPreMemo = ({ VariableSizeGridProps: { style, rowIndex, columnIndex, da
const handleContextClick = useCallback(
(event) => {
event.preventDefault();
if(item.disableFilterMenu){
event.stopPropagation();
if(item?.disableFilterMenu){
return;
}
contextMenuLocationRef.current.x = event.clientX;
contextMenuLocationRef.current.y = event.clientY;
if(data.onRowContextMenuClick){
const newMenuItems = data.onRowContextMenuClick({rowDataStatic: data.items[rowIndex][columnIndex]?.props?.rowData});
if(newMenuItems.length > 0){
setContextMenuOptions(newMenuItems);
Promise.resolve(newMenuItems).then(function(value) {
if(value.length > 0){
setContextMenuOptions(value);
setOpenContextMenu(true);
}
})
} else {
if(contextMenuOptions && contextMenuOptions.length > 0){
setOpenContextMenu(true);
return;
}
}
if(contextMenuOptions && contextMenuOptions.length > 0){
setOpenContextMenu(true);
}
},
[contextMenuOptions, data.onRowContextMenuClick] // eslint-disable-line react-hooks/exhaustive-deps
);
return (
<div style={{...style, ...cellStyle, ...rowStyle}}
className={`${classes.cell} ${rowClassName} ${selectedClass}`}
className={`${classes.cell} ${rowClassName} ${rowHighlight} ${selectedClass}`}
onDoubleClick={handleDoubleClick}
onClick={handleClick}
onMouseEnter={onMouseEnter}
@@ -87,14 +95,14 @@ const CellPreMemo = ({ VariableSizeGridProps: { style, rowIndex, columnIndex, da
{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;
@@ -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);
@@ -29,6 +30,8 @@ const DraggableHandles = React.forwardRef(({ height, rowHeight, width, minColumn
top: 0,
bottom: 0,
}}
offsetParent={document.body}
defaultPosition={{x: 0, y: 0}}
position={{x: 0, y: 0}}
onStart={() => {
@@ -38,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>
);
})}
@@ -52,4 +56,4 @@ const DraggableHandles = React.forwardRef(({ height, rowHeight, width, minColumn
);
});
export default DraggableHandles;
export default DraggableHandles;
@@ -5,13 +5,7 @@ import {classes} from './styles';
import React from 'react';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faFilter} from '@fortawesome/free-solid-svg-icons';
import Grow from '@mui/material/Grow';
import Popper from '@mui/material/Popper';
import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import Paper from '@mui/material/Paper';
import {useTheme} from '@mui/material/styles';
import {ContextMenu} from "./Cell";
const HeaderCell = ({
onClick = () => {},
@@ -23,9 +17,9 @@ const HeaderCell = ({
VariableSizeGridProps: { style, rowIndex, columnIndex, data },
}) => {
const dropdownAnchorRef = React.useRef(null);
const theme = useTheme();
const item = data.items[rowIndex][columnIndex];
const isFiltered = item?.filtered || false;
const contextMenuLocationRef = React.useRef({x: 0, y: 0});
const handleClick = useCallback(
(e) => {
onClick(e, columnIndex);
@@ -47,22 +41,20 @@ const HeaderCell = ({
return;
}
if(contextMenuOptions && contextMenuOptions.length > 0){
contextMenuLocationRef.current.x = event.clientX;
contextMenuLocationRef.current.y = event.clientY;
setOpenContextMenu(true);
}
},
[contextMenuOptions, columnIndex] // eslint-disable-line react-hooks/exhaustive-deps
);
const handleMenuItemClick = (event, index) => {
contextMenuOptions[index].click({event, columnIndex});
const handleMenuItemClick = (event, clickOption) => {
event.preventDefault();
event.stopPropagation();
clickOption({event, columnIndex, rowIndex, data: data.items[rowIndex][columnIndex]?.props?.rowData || {}});
setOpenContextMenu(false);
};
const handleClose = (event) => {
if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
return;
}
setOpenContextMenu(false);
};
const handleClicks = useSingleAndDoubleClick(handleClick, handleDoubleClick);
return (
@@ -73,31 +65,11 @@ const HeaderCell = ({
</Typography>
{isFiltered && <FontAwesomeIcon icon={faFilter} />}
{sortIndicatorIndex === columnIndex && (sortDirection === 'ASC' ? <div>↑</div> : <div>↓</div>)}
<Popper open={openContextMenu} anchorEl={dropdownAnchorRef.current} role={undefined} transition disablePortal style={{zIndex: 4}}>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
}}
>
<Paper variant="outlined" style={{backgroundColor: theme.palette.mode === 'dark' ? theme.palette.primary.dark : theme.palette.primary.light}}>
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
<MenuList id="split-button-menu" >
{contextMenuOptions.map((option, index) => (
<MenuItem
key={option.name + index}
onClick={(event) => handleMenuItemClick(event, index)}
>
{option.name}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
<ContextMenu dropdownAnchorRef={dropdownAnchorRef} contextMenuOptions={contextMenuOptions}
disableFilterMenu={item?.disableFilterMenu} openContextMenu={openContextMenu}
contextMenuLocationRef={contextMenuLocationRef}
setOpenContextMenu={setOpenContextMenu} handleMenuItemClick={handleMenuItemClick}
/>
</Box>
</div>
);
@@ -7,19 +7,26 @@ import HeaderCell from './HeaderCell';
import Cell from './Cell';
import DraggableHandles from './DraggableHandles';
import {classes} from './styles';
import {GetMythicSetting, SetMythicSetting} from "../MythicSavedUserSetting";
import {GetMythicSetting, useSetMythicSetting} from "../MythicSavedUserSetting";
const HeaderCellContext = createContext({});
const MIN_COLUMN_WIDTH = 50;
const MIN_COLUMN_WIDTH = 100;
const MIN_FLEX_COLUMN_WIDTH = 150;
const CellRenderer = (VariableSizeGridProps) => {
return VariableSizeGridProps.rowIndex === 0 ? null : <Cell VariableSizeGridProps={VariableSizeGridProps} />;
const CellRendererPreMemo = ({ style, rowIndex, columnIndex, data }) => {
return rowIndex === 0 ? null : <Cell style={style} rowIndex={rowIndex} columnIndex={columnIndex} data={data} />;
};
const innerElementType = React.forwardRef(({ children, ...rest }, ref) => {
const CellRenderer = React.memo(CellRendererPreMemo);
const innerElementType = React.forwardRef(({ children, style }, ref) => {
const HeaderCellData = useContext(HeaderCellContext);
const onHeaderDoubleClick = React.useCallback( (e, columnIndex) => {
if (HeaderCellData.columns[columnIndex].disableAutosize) return;
HeaderCellData.autosizeColumn({columnIndex});
}, [HeaderCellData.columns, HeaderCellData.autosizeColumn]);
return (
<div ref={ref} {...rest}>
<div ref={ref} style={style}>
{/* always render header cells */}
<div
className={classes.headerCellRow}
@@ -33,10 +40,7 @@ const innerElementType = React.forwardRef(({ children, ...rest }, ref) => {
key={i}
headerNameKey={HeaderCellData.headerNameKey}
onClick={HeaderCellData.onClickHeader}
onDoubleClick={(e, columnIndex) => {
if (column.disableAutosize) return;
HeaderCellData.autosizeColumn({columnIndex});
}}
onDoubleClick={onHeaderDoubleClick}
contextMenuOptions={HeaderCellData.contextMenuOptions}
sortIndicatorIndex={HeaderCellData.sortIndicatorIndex}
sortDirection={HeaderCellData.sortDirection}
@@ -62,7 +66,7 @@ const innerElementType = React.forwardRef(({ children, ...rest }, ref) => {
);
});
const getShortRandomString = () => {
export const GetShortRandomString = () => {
return (Math.random() + 1).toString(36).substring(2);
}
const ResizableGridWrapper = ({
@@ -97,9 +101,10 @@ const ResizableGridWrapper = ({
}
return Math.max(column?.width || 0, MIN_COLUMN_WIDTH);
}));
const gridUUID = React.useMemo( () => getShortRandomString(), []);
const gridUUID = React.useMemo( () => GetShortRandomString(), []);
const gridRef = useRef(null);
const dragHandlesRef = useRef(null);
const [updateSetting] = useSetMythicSetting();
const getColumnWidth = useCallback(
(index) => {
return columnWidths[index] || MIN_COLUMN_WIDTH;
@@ -142,16 +147,22 @@ const ResizableGridWrapper = ({
//updatedColumnWidths[updatedWidthIndex] += totalWidthDiff;
}
setColumnWidths(updatedColumnWidths);
SetMythicSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths, output: "json-array"});
if(name !== undefined){
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
}
}, [scrollbarWidth, columns, AutoSizerProps.width, localColumnsRef.current]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
gridRef.current.resetAfterColumnIndex(0, true);
if(name !== undefined){
updateSetting({setting_name: `${name}_column_widths`, value: columnWidths});
}
}, [columnWidths]);
/* Event Handlers */
const resizeColumn = (x, columnIndex) => {
const resizeColumn = useCallback( (x, columnIndex) => {
const updatedColumnWidths = columnWidths.map((columnWidth, index) => {
if (columnIndex === index) {
return Math.floor(Math.max(columnWidth + x, MIN_COLUMN_WIDTH));
@@ -159,21 +170,24 @@ const ResizableGridWrapper = ({
return Math.floor(columnWidth);
});
setColumnWidths(updatedColumnWidths);
SetMythicSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths, output: "json-array"});
};
const autosizeColumn = ({columnIndex}) => {
if(name !== undefined){
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
}
}, [columnWidths]);
const autosizeColumn = useCallback( ({columnIndex}) => {
if(columns[columnIndex].disableDoubleClick){
return
}
const longestElementInColumn = Math.max(...items.map((itemRow) => {
if(!columns[columnIndex].key){
if(columns[columnIndex].key === undefined){
if(columns[columnIndex].plaintext){
columns[columnIndex].key = columns[columnIndex].plaintext;
} else {
return 30;
}
}
if(columns[columnIndex].key){
if(columns[columnIndex].key !== undefined){
if(columns[columnIndex].key.includes("time")){
return 30;
}
@@ -181,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;
@@ -194,6 +211,10 @@ const ResizableGridWrapper = ({
}catch(error){
//console.log(itemRow[columnIndex]?.props?.rowData?.[columns[columnIndex].key])
}
if(typeof itemRow[columnIndex]?.props?.cellData === 'string' ){
return itemRow[columnIndex]?.props?.cellData.length;
}
let data = itemRow[columnIndex]?.props?.rowData?.[columns[columnIndex].key];
if(columns[columnIndex].inMetadata){
return itemRow[columnIndex]?.props?.cellData.length;
@@ -203,8 +224,10 @@ const ResizableGridWrapper = ({
}
if(data.plaintext){
return String(data.plaintext)?.length;
} else if(data?.button?.name) {
return String(data?.button?.name)?.length ;
} else {
return String(data)?.length ;
return MIN_COLUMN_WIDTH;
}
//return String(itemRow[columnIndex]?.props?.rowData?.[columns[columnIndex].key]).length || -1;
} else if(typeof(itemRow[columnIndex]?.props?.cellData) === "string") {
@@ -232,10 +255,18 @@ const ResizableGridWrapper = ({
}
return Math.floor(columnWidth);
});
//console.log(updatedWidths, longestElementInColumn);
setColumnWidths(updatedColumnWidths);
SetMythicSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths, output: "json-array"});
};
//console.log(updatedColumnWidths, columnWidths, longestElementInColumn);
let updatedValues = false;
for(let i = 0; i < updatedColumnWidths.length; i++){
if(updatedColumnWidths[i] !== columnWidths[i]){
updatedValues = true;
break;
}
}
if(updatedValues){
setColumnWidths(() => updatedColumnWidths);
}
}, [columnWidths]);
useEffect( () => {
if(callbackTableGridRef){
@@ -255,7 +286,13 @@ const ResizableGridWrapper = ({
"sortDirection": sortDirection,
"getColumnWidth": getColumnWidth,
"itemsWithHeader": itemsWithHeader,
}
};
const localOnScroll = React.useCallback( ({scrollLeft}) => {
if (dragHandlesRef.current) {
dragHandlesRef.current.scrollTo({ left: scrollLeft });
}
}, [dragHandlesRef.current])
return (
<>
<HeaderCellContext.Provider value={headerCellData}>
@@ -272,12 +309,9 @@ const ResizableGridWrapper = ({
rowContextMenuOptions, onRowClick,
onRowContextMenuClick}}
innerElementType={innerElementType}
overscanRowCount={10}
onScroll={({ scrollLeft }) => {
if (dragHandlesRef.current) {
dragHandlesRef.current.scrollTo({ left: scrollLeft });
}
}}
overscanRowCount={20}
useIsScrolling={false}
onScroll={localOnScroll}
ref={gridRef}>
{CellRenderer}
</VariableSizeGrid>
@@ -1,7 +1,22 @@
import { useReactiveVar } from '@apollo/client';
import { meState } from '../../cache';
import {mePreferences, operatorSettingDefaults} from '../../cache';
import React from 'react';
import {useMutation, gql } from '@apollo/client';
import {snackActions} from "../utilities/Snackbar";
const updatePreferences = gql`
mutation updatePreferences($preferences: jsonb!){
updateOperatorPreferences(preferences: $preferences){
status
error
}
}
`;
export const GetComputedFontSize = () => {
const element = document.getElementById('root');
const fontSizeString = window.getComputedStyle(element).fontSize;
return parseFloat(fontSizeString);
}
/*
setting_name options:
hideUsernames
@@ -14,123 +29,55 @@ setting_name options:
callbacks_table_columns
callbacks_table_filters
*/
export function useMythicSetting({setting_name, default_value, output="boolean"}){
const me = useReactiveVar(meState);
// get the initial value we have stored
const localStorageSetting = localStorage.getItem(`${me?.user?.user_id || 0}-${setting_name}`);
let initialStorageSetting = localStorageSetting === null ? default_value : localStorageSetting;
switch(output){
case "boolean":
initialStorageSetting = (initialStorageSetting.toLowerCase() === "true");
break;
case "number":
initialStorageSetting = Number(initialStorageSetting);
break;
case "json-array":
try{
initialStorageSetting = JSON.parse(initialStorageSetting);
}catch(error){
console.log(initialStorageSetting);
}
break;
case "json":
try{
initialStorageSetting = JSON.parse(initialStorageSetting);
}catch(error){
console.log(initialStorageSetting);
}
break;
case "string":
break;
default:
console.log("unknown output type", output);
}
export function useGetMythicSetting({setting_name, default_value}){
const preferences = useReactiveVar(mePreferences);
let initialStorageSetting = GetMythicSetting({setting_name, default_value});
const [setting, setSetting] = React.useState(initialStorageSetting);
React.useEffect( () => {
// update the initial value if the user changes
const localStorageSetting = localStorage.getItem(`${me?.user?.user_id || 0}-${setting_name}`);
let initialStorageSetting = localStorageSetting === null ? default_value : localStorageSetting;
switch(output){
case "boolean":
initialStorageSetting = (initialStorageSetting.toLowerCase() === "true");
break;
case "number":
initialStorageSetting = Number(initialStorageSetting);
break;
case "json-array":
try{
initialStorageSetting = JSON.parse(initialStorageSetting);
}catch(error){
initialStorageSetting = [];
}
break;
case "json":
try{
initialStorageSetting = JSON.parse(initialStorageSetting);
}catch(error){
initialStorageSetting = {};
}
break;
case "string":
break;
default:
console.log("unknown output type", output);
}
setSetting(initialStorageSetting);
}, [me]);
let newSetting = GetMythicSetting({setting_name, default_value});
setSetting(newSetting);
}, [preferences?.[setting_name]]);
return setting;
}
export function GetMythicSetting({setting_name, default_value, output="boolean"}){
const me = meState();
// get the initial value we have stored
const localStorageSetting = localStorage.getItem(`${me?.user?.user_id || 0}-${setting_name}`);
let initialStorageSetting = localStorageSetting === null ? default_value : localStorageSetting;
switch(output){
case "boolean":
initialStorageSetting = (initialStorageSetting.toLowerCase() === "true");
break;
case "number":
initialStorageSetting = Number(initialStorageSetting);
break;
case "json-array":
try{
initialStorageSetting = JSON.parse(initialStorageSetting);
}catch(error){
console.log(initialStorageSetting);
}
break;
case "json":
try{
initialStorageSetting = JSON.parse(initialStorageSetting);
}catch(error){
console.log(initialStorageSetting);
}
break;
case "string":
break;
default:
console.log("unknown output type", output);
}
return initialStorageSetting;
export function GetMythicSetting({setting_name, default_value}){
const preferences = mePreferences();
return preferences?.[setting_name] === undefined ? default_value : preferences?.[setting_name];
}
export function SetMythicSetting({setting_name, value, output = "boolean"}) {
let newSetting = value;
switch(output){
case "json-array":
newSetting = JSON.stringify(value);
break;
case "json":
newSetting = JSON.stringify(value);
break;
case "boolean":
if(typeof value === "boolean"){
newSetting = value ? "true" : "false";
} else {
newSetting = value.toLowerCase() === "true" ? "true" : "false";
export function useSetMythicSetting() {
const [updateSetting] = useMutation(updatePreferences, {
onCompleted: (data) => {
//console.log(data, mePreferences());
},
onError: (error) => {
snackActions.error("failed to save user setting: " + error.message);
console.log(error);
}
});
return [
({setting_name, value}) => {
if(mePreferences()?.[setting_name] !== value){
const updatedPreferences = {
...mePreferences(),
[setting_name]: value,
};
mePreferences(updatedPreferences);
updateSetting({variables: {preferences: updatedPreferences}});
}
break;
default:
}
const meID = meState()?.user?.user_id || 0;
localStorage.setItem(`${meID}-${setting_name}`, newSetting)
},
({settings}) => {
const updatedPreferences = {
...mePreferences(),
...settings,
};
mePreferences(updatedPreferences);
updateSetting({variables: {preferences: updatedPreferences}});
},
() => {
mePreferences(operatorSettingDefaults);
updateSetting({variables: {preferences: operatorSettingDefaults}});
}
]
}
@@ -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
@@ -1,29 +1,17 @@
import React from 'react';
import { Tooltip } from '@mui/material';
import {useTheme} from '@mui/material/styles';
export function MythicStyledTooltip(props){
const { children, title, style, maxWidth, placement, enterDelay, ...other} = props;
export function MythicStyledTooltip({ children, title, enterDelay, tooltipStyle}){
const theme = useTheme();
return (
<Tooltip title={title} arrow followCursor enterDelay={enterDelay ? enterDelay : 750}
placement={placement ? placement : "bottom"}
componentsProps={{
tooltip: {
sx: {
backgroundColor: theme.palette.background.contrast,
color: theme.palette.text.contrast,
boxShadow: theme.shadows[1],
fontSize: 13,
'& .MuiTooltip-arrow': {
color: theme.palette.background.contrast
},
maxWidth: maxWidth ? maxWidth : "300px",
zIndex: 100000
}
}
}} style={{zIndex:100000}} {...other}>
{<span style={{display: "inline-block", ...style}}>{children}</span>}
</Tooltip>
);
<span style={{display: "inline-block", ...tooltipStyle}}
data-tooltip-id={"my-tooltip"}
data-tooltip-content={title}
data-tooltip-variant={theme.palette.mode === 'dark' ? 'light' : 'dark'}
data-tooltip-delay-show={enterDelay ? enterDelay : 750}
>
{children}
</span>
)
}
@@ -8,10 +8,13 @@ import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import Paper from '@mui/material/Paper';
import {useTheme} from '@mui/material/styles';
import Badge from '@mui/material/Badge';
import {
Draggable,
} from "@hello-pangea/dnd";
export function MythicTabPanel(props) {
const { children, value, index, maxHeight, tabInfo, getCallbackData, queryParams, changeSearchParam, ...other } =
const { children, value, index, maxHeight, tabInfo, getCallbackData, queryParams, changeSearchParam, showDeleted, ...other } =
props;
const style =
props.style === undefined
@@ -36,68 +39,12 @@ export function MythicTabPanel(props) {
</div>
);
}
function a11yProps(index) {
export function a11yProps(index) {
return {
id: `scrollable-auto-tab-${index}`,
'aria-controls': `scrollable-auto-tabpanel-${index}`,
};
}
function allowDrop(ev) {
ev.preventDefault();
return true;
}
function drag(ev) {
ev.dataTransfer.setData("text", ev.target.id);
//ev.dataTransfer.setData("opacity", ev.target.style.opacity);
//ev.target.style.opacity = "0.2";
}
function onDragEnter(ev){
let node = ev.target.getAttribute("role") === "tab" ? ev.target : ev.target.closest('[role="tab"]');
node.style.borderLeft = "3px dotted red";
//console.log("ondragenter");
}
function onDragLeave(ev){
let node = ev.target.closest('[role="tab"]');
//console.log("onDragLeave");
if(ev.target.getAttribute("role") === "tab"){
//console.log("leaving tab", ev.target)
node.style.border = "";
} else if(node.contains(ev.target)){
//node.style.border = "";
//console.log("onDragLeave, nodeContains", node, ev.target)
}else{
node.style.border = "";
//console.log("onDragLeave, not contains", node, ev.target)
}
}
function onDragStop(ev) {
let node = ev.target.getAttribute("role") === "tab" ? ev.target : ev.target.closest('[role="tab"]');
node.style.border = "";
}
function drop(ev) {
// shares dataTransfer with drag function
//console.log("drop")
ev.preventDefault();
const data = ev.dataTransfer.getData("text");
const tabList = ev.target.closest("div[role='tablist']");
let node = ev.target.getAttribute("role") === "tab" ? ev.target : ev.target.closest('[role="tab"]');
//console.log(tabList, data, node.nextSibling);
node.style.border = "";
//node.style.opacity = 1;
//document.getElementById(data).style.opacity = ev.dataTransfer.getData("opacity");
for(let i = 0; i < tabList.children.length; i++){
tabList.children[i].style.border = "";
//tabList.children[i].style.opacity = 1;
}
//tabList.insertBefore(document.getElementById(data), node.nextSibling);
//console.log("selected", data, "toLeftOf", node.id);
return {"selected": data, "toLeftOf": node.id};
}
export function MythicTabLabel(props) {
const {
label,
@@ -114,17 +61,20 @@ export function MythicTabLabel(props) {
getCallbackData,
onDragTab,
selectedIndex,
highlight,
newDataForTab,
...other
} = props;
const onClick = (e) => {
e.stopPropagation();
e.preventDefault();
onCloseTab({ tabID: tabInfo.tabID, index: index });
};
const theme = useTheme();
const [openContextMenu, setOpenContextMenu] = React.useState(false);
const dropdownAnchorRef = React.useRef(null);
const handleContextClick = (event) => {
event.preventDefault();
event.stopPropagation();
if(contextMenuOptions && contextMenuOptions.length > 0){
setOpenContextMenu(true);
}
@@ -140,53 +90,58 @@ export function MythicTabLabel(props) {
setOpenContextMenu(false);
};
return (
<Tab
onDrop={(ev) => onDragTab(drop(ev))}
onDragOver={allowDrop}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
draggable={!!onDragTab}
className={selectedIndex === index ? "selectedCallback" : "" }
onDragStart={drag}
onDragEnd={onDragStop}
label={
<span onContextMenu={handleContextClick} style={{ display: 'inline-block', zIndex: 1}} ref={dropdownAnchorRef}>
{label}
<IconButton component='div' size='small' onClick={onClick} {...other}>
<CloseIcon />
</IconButton>
<Draggable draggableId={`callbacks-tab-${tabInfo.tabID}`} index={index} disableInteractiveElementBlocking={true}>
{(provided, snapshot) => (
<div ref={provided.innerRef}
{...provided.draggableProps}>
<Tab
className={selectedIndex === index || snapshot.isDragging ? "selectedCallback" : "" }
label={
<span onContextMenu={handleContextClick} style={{}} ref={dropdownAnchorRef}>
<Badge color="success" variant="dot" invisible={!highlight} >
{label}
</Badge>
<IconButton component='div' size='small' onClick={onClick} {...other}>
<CloseIcon />
</IconButton>
</span>
}
{...a11yProps(index)}
{...other}
{...provided.dragHandleProps}
style={{padding: "0px 5px 0px 5px", borderRadius: "4px", margin: 0,
borderBottom: selectedIndex === index ? `2px solid grey` : '',
backgroundColor: selectedIndex === index ? tabInfo.color : snapshot.isDragging ? tabInfo.color : ""}}
/>
<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" className={"dropdownMenuColored"}>
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
<MenuList id="split-button-menu" >
{contextMenuOptions.map((option, index) => (
<MenuItem
key={option.name + index}
onClick={(event) => handleMenuItemClick(event, index)}
>
{option.name}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</span>
}
{...a11yProps(index)}
{...other}
style={{padding: "0px 5px 0px 5px", borderRadius: "4px", margin: 0}}
/>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
}}
>
<Paper variant="outlined" className={"dropdownMenuColored"}>
<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>
</div>
)}
</Draggable>
);
}
export function MythicSearchTabLabel(props) {
@@ -1,6 +1,6 @@
import {styled} from '@mui/material/styles';
import TableCell from '@mui/material/TableCell';
const MythicStyledTableCell = styled(TableCell, {name: "StyledTableRow", slot: "Wrapper"})({
padding: "0 0 0 10px",
const MythicStyledTableCell = styled(TableCell, {})({
padding: "2px 0 2px 10px !important",
});
export default MythicStyledTableCell;
@@ -26,14 +26,20 @@ import LocalOfferOutlinedIcon from '@mui/icons-material/LocalOfferOutlined';
import {MythicStyledTooltip} from "./MythicStyledTooltip";
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
import Typography from '@mui/material/Typography';
import MythicStyledTableCell from "./MythicTableCell";
import {meState} from "../../cache";
import { useReactiveVar } from '@apollo/client';
import {MythicDraggableDialogTitle} from "./MythicDraggableDialogTitle";
const createNewTagMutationTemplate = ({target_object}) => {
// target_object should be something like "task_id"
// target_object_id should be something like "89"
return gql`
mutation createNewTag($url: String!, $data: jsonb!, $source: String!, $${target_object}: Int!, $tagtype_id: Int!){
insert_tag_one(object: {url: $url, data: $data, source: $source, ${target_object}: $${target_object}, tagtype_id: $tagtype_id}){
createTag(url: $url, data: $data, source: $source, ${target_object}: $${target_object}, tagtype_id: $tagtype_id){
id
status
error
}
}
`;
@@ -94,6 +100,8 @@ query getSingleTag($tag_id: Int!){
mythictree_id
operation_id
response_id
callback_id
payload_id
task_id
taskartifact_id
tagtype {
@@ -105,15 +113,16 @@ query getSingleTag($tag_id: Int!){
}
}
`
export const TagsDisplay = ({tags}) => {
export const TagsDisplay = ({tags, expand}) => {
return (
tags?.map( tt => (
<TagChipDisplay tag={tt} key={tt.id} />
<TagChipDisplay tag={tt} key={tt.id} expand={expand} />
))
)
}
const TagChipDisplay = ({tag}) => {
const TagChipDisplay = ({tag, expand}) => {
const [openTagDisplay, setOpenTagDisplay] = React.useState(false);
const [label, setLabel] = React.useState(expand ? tag.tagtype.name : tag.tagtype.name[0]);
const onSelectTag = (event, tag) => {
if(event){
event.preventDefault();
@@ -128,10 +137,22 @@ const TagChipDisplay = ({tag}) => {
}
setOpenTagDisplay(false);
}
const onMouseOver = () => {
if(expand === undefined || !expand){
setLabel(tag.tagtype.name);
}
}
const onMouseOut = () => {
if(expand === undefined || !expand){
setTimeout( () => {
setLabel(tag.tagtype.name[0]);
}, 10000); // wait 10s then go back to just a single letter
}
}
return (
<React.Fragment>
<Chip label={tag.tagtype.name} size="small" onClick={(e) => onSelectTag(e)}
style={{float: "right", backgroundColor:tag.tagtype.color,}}
<Chip onMouseOver={onMouseOver} onMouseOut={onMouseOut} label={label} size="small" onClick={(e) => onSelectTag(e)}
style={{float: "right", backgroundColor:tag.tagtype.color, height: "15px"}}
sx={{
"& .MuiChip-label": {overflow: "visible"}
}}
@@ -222,6 +243,10 @@ function ViewTagDialog(props) {
setObjectInfo({object_type: "task_id", object_id: data.tag_by_pk.task_id});
}else if(data.tag_by_pk.taskartifact_id !== null){
setObjectInfo({object_type: "taskartifact_id", object_id: data.tag_by_pk.taskartifact_id});
}else if(data.tag_by_pk.payload_id !== null){
setObjectInfo({object_type: "payload_id", object_id: data.tag_by_pk.payload_id});
}else if(data.tag_by_pk.callback_id !== null){
setObjectInfo({object_type: "callback_id", object_id: data.tag_by_pk.callback_id});
}
let newTag = {...data.tag_by_pk};
let tagData = newTag;
@@ -252,11 +277,14 @@ function ViewTagDialog(props) {
}
props.onClose(event);
}
const stopClicks = (e) => {
e.stopPropagation();
e.preventDefault();
}
return (
<React.Fragment>
<DialogTitle id="form-dialog-title" onClick={(e) => e.stopPropagation()}>View Tag</DialogTitle>
<TableContainer className="mythicElement">
<DialogTitle id="form-dialog-title" onClick={stopClicks}>View Tag</DialogTitle>
<TableContainer className="mythicElement" onClick={stopClicks}>
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
<TableBody>
<TableRow hover>
@@ -271,20 +299,20 @@ return (
<TableCell>{selectedTag?.tagtype?.description || ""}</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Source</TableCell>
<TableCell>
<MythicStyledTableCell>Source</MythicStyledTableCell>
<MythicStyledTableCell>
{selectedTag?.source ||""}
</TableCell>
</MythicStyledTableCell>
</TableRow>
<TableRow hover>
<TableCell>Reference URL</TableCell>
<TableCell>
<MythicStyledTableCell>Reference URL</MythicStyledTableCell>
<MythicStyledTableCell>
{selectedTag?.url === "" ? (
"No reference link provided"
) : (
<Link href={selectedTag?.url || "#"} color="textPrimary" target="_blank" referrerPolicy='no'>{selectedTag?.url ? "click here" : "No reference link provided"}</Link>
)}
</TableCell>
</MythicStyledTableCell>
</TableRow>
<TableRow>
<TableCell>Data</TableCell>
@@ -295,31 +323,36 @@ return (
<TableBody>
{Object.keys(selectedTag.data).map( key => (
<TableRow key={key} hover>
<TableCell>{key}</TableCell>
<MythicStyledTableCell>{key}</MythicStyledTableCell>
{typeof selectedTag.data[key] === "string" ? (
<TableCell style={{whiteSpace: "pre-wrap"}}>
<MythicStyledTableCell style={{whiteSpace: "pre-wrap"}}>
<StringTagDataEntry name={key} value={String(selectedTag.data[key])} />
</TableCell>
</MythicStyledTableCell>
) : typeof selectedTag.data[key] === "object" ? (
Array.isArray(selectedTag.data[key]) ? (
<TableCell style={{whiteSpace: "pre-wrap"}}>{JSON.stringify(selectedTag.data[key], null, 2)}</TableCell>
) : (
<Table size={"small"} >
{Object.keys(selectedTag.data[key]).map(key2 => (
<TableRow key={key2} >
<TableCell>{key2}</TableCell>
<TableCell>
<StringTagDataEntry name={key2} value={String(selectedTag.data[key][key2])} />
</TableCell>
</TableRow>
))}
</Table>
<MythicStyledTableCell>
<Table size={"small"} >
<TableBody>
{Object.keys(selectedTag.data[key]).map(key2 => (
<TableRow key={key2} >
<MythicStyledTableCell>{key2}</MythicStyledTableCell>
<MythicStyledTableCell>
<StringTagDataEntry name={key2} value={String(selectedTag.data[key][key2])} />
</MythicStyledTableCell>
</TableRow>
))}
</TableBody>
</Table>
</MythicStyledTableCell>
)
) : typeof selectedTag.data[key] === "boolean" ? (
<TableCell>{selectedTag.data[key] ? "True" : "False"}</TableCell>
<MythicStyledTableCell>{selectedTag.data[key] ? "True" : "False"}</MythicStyledTableCell>
) :
(
<TableCell>{String(selectedTag.data[key])}</TableCell>
<MythicStyledTableCell>{String(selectedTag.data[key])}</MythicStyledTableCell>
)
}
@@ -377,7 +410,11 @@ export function ViewEditTagsDialog(props) {
setNewSource(data.tag[0].source);
setNewURL(data.tag[0].url);
try{
setNewData(JSON.stringify(data.tag[0].data, null, 2));
if(typeof data.tag[0].data !== "string"){
setNewData(JSON.stringify(data.tag[0].data, null, 2));
} else {
setNewData(String(data.tag[0].data));
}
}catch(error){
setNewData(String(data.tag[0].data));
}
@@ -459,24 +496,21 @@ const onAcceptDelete = () => {
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Edit Tags
</DialogTitle>
<DialogContent dividers={true}>
{openNewDialog ?
(<MythicDialog fullWidth={true} maxWidth="xl" open={openNewDialog}
<MythicDraggableDialogTitle >Edit Tags </MythicDraggableDialogTitle>
<DialogContent dividers={true} style={{width: "100%"}}>
{openNewDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openNewDialog}
onClose={()=>{setOpenNewDialog(false);}}
innerDialog={<NewTagDialog me={props.me}
target_object={props.target_object}
target_object_id={props.target_object_id}
onClose={()=>{setOpenNewDialog(false);}}
onSubmit={handleNewTagCreate} />}
/>) : null}
<TableContainer className="mythicElement">
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
/>}
<Table size="small" style={{ width: "100%", overflow: "scroll"}}>
<TableBody>
<TableRow hover>
<TableCell style={{width: "30%"}}>Select Existing Tag to Edit or Add New</TableCell>
<TableCell style={{display: "inline-flex", flexDirection: "row-reverse"}}>
<MythicStyledTableCell style={{width: "30%"}}>Select Existing Tag to Edit or Add New</MythicStyledTableCell>
<MythicStyledTableCell style={{display: "inline-flex", flexDirection: "row-reverse"}}>
<MythicStyledTooltip title={"Add New Tag"}>
<IconButton variant='contained' color="success" style={{float: "right"}} onClick={() => setOpenNewDialog(true)} >
<AddCircleOutlineIcon />
@@ -501,28 +535,28 @@ return (
{openDelete &&
<MythicConfirmDialog onClose={() => {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete} open={openDelete}/>
}
</TableCell>
</MythicStyledTableCell>
</TableRow>
<TableRow hover>
<TableCell>Tag Description</TableCell>
<TableCell>{selectedTag?.tagtype?.description || ""}</TableCell>
<MythicStyledTableCell>Tag Description</MythicStyledTableCell>
<MythicStyledTableCell>{selectedTag?.tagtype?.description || ""}</MythicStyledTableCell>
</TableRow>
<TableRow hover>
<TableCell>Source</TableCell>
<TableCell>
<MythicStyledTableCell>Source</MythicStyledTableCell>
<MythicStyledTableCell>
<MythicTextField value={newSource} onChange={onChangeSource} name="Source of tag data" />
</TableCell>
</MythicStyledTableCell>
</TableRow>
<TableRow hover>
<TableCell>External URL</TableCell>
<TableCell>
<MythicStyledTableCell>External URL</MythicStyledTableCell>
<MythicStyledTableCell>
<MythicTextField value={newURL} onChange={onChangeURL} name="External URL reference" />
<Link href={newURL} color="textPrimary" target="_blank" referrerPolicy='no'>{newURL ? "click here" : ""}</Link>
</TableCell>
</MythicStyledTableCell>
</TableRow>
<TableRow hover>
<TableCell>JSON Data</TableCell>
<TableCell>
<MythicStyledTableCell>JSON Data</MythicStyledTableCell>
<MythicStyledTableCell>
<AceEditor
mode="json"
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
@@ -538,11 +572,10 @@ return (
tabSize: 4,
useWorker: false
}}/>
</TableCell>
</MythicStyledTableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
@@ -576,9 +609,14 @@ export function NewTagDialog(props) {
});
const [newTag] = useMutation(createNewTagMutationTemplate({target_object: props.target_object}), {
onCompleted: data => {
snackActions.success("Successfully created new tag!");
props.onSubmit({source:newSource, url:newURL, data:newData, tagtype_id:selectedTagType.id, id: data.insert_tag_one.id});
props.onClose()
if(data.createTag.status === "success"){
snackActions.success("Successfully created new tag!");
props.onSubmit({source:newSource, url:newURL, data:newData, tagtype_id:selectedTagType.id, id: data.createTag.id});
props.onClose()
} else {
snackActions.error(data.createTag.error);
}
},
onError: error => {
snackActions.error(error.message);
@@ -606,13 +644,12 @@ export function NewTagDialog(props) {
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Add New Tag</DialogTitle>
<DialogContent dividers={true}>
<TableContainer className="mythicElement">
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
<MythicDraggableDialogTitle >Add New Tag</MythicDraggableDialogTitle>
<DialogContent dividers={true} style={{width: "100%"}}>
<Table size="small" style={{ overflow: "scroll", width: "100%"}}>
<TableBody>
<TableRow hover>
<TableCell style={{width: "20%"}}>
<MythicStyledTableCell style={{width: "20%"}}>
<Typography>
Tag
</Typography>
@@ -624,8 +661,8 @@ export function NewTagDialog(props) {
here
</Link>
</Typography>
</TableCell>
<TableCell>
</MythicStyledTableCell>
<MythicStyledTableCell>
<Select
labelId="demo-dialog-select-label"
id="demo-dialog-select"
@@ -639,24 +676,24 @@ export function NewTagDialog(props) {
</MenuItem>
) )}
</Select>
</TableCell>
</MythicStyledTableCell>
</TableRow>
<TableRow hover>
<TableCell>Source</TableCell>
<TableCell>
<MythicStyledTableCell>Source</MythicStyledTableCell>
<MythicStyledTableCell>
<MythicTextField value={newSource} onChange={onChangeSource} name="Source of tag data" />
</TableCell>
</MythicStyledTableCell>
</TableRow>
<TableRow hover>
<TableCell>External URL</TableCell>
<TableCell>
<MythicStyledTableCell>External URL</MythicStyledTableCell>
<MythicStyledTableCell>
<MythicTextField value={newURL} onChange={onChangeURL} name="External URL reference" />
<Link href={newURL} color="textPrimary" target="_blank" referrerPolicy='no'>{newURL}</Link>
</TableCell>
</MythicStyledTableCell>
</TableRow>
<TableRow hover>
<TableCell>JSON Data</TableCell>
<TableCell>
<MythicStyledTableCell>JSON Data</MythicStyledTableCell>
<MythicStyledTableCell>
<AceEditor
mode="json"
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
@@ -672,11 +709,10 @@ export function NewTagDialog(props) {
tabSize: 4,
useWorker: false
}}/>
</TableCell>
</MythicStyledTableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
@@ -692,7 +728,8 @@ export function NewTagDialog(props) {
</React.Fragment>
);
}
export const ViewEditTags = ({target_object, target_object_id, me}) => {
export const ViewEditTags = ({target_object, target_object_id}) => {
const me = useReactiveVar(meState);
const [openTagDialog, setOpenTagDialog] = React.useState(false);
const toggleTagDialog = (event, open) => {
if(event){
@@ -703,7 +740,10 @@ export const ViewEditTags = ({target_object, target_object_id, me}) => {
}
return(
<React.Fragment>
<IconButton onClick={(e) => toggleTagDialog(e, true)} size="small" style={{display: "inline-block", float: "right"}}><LocalOfferOutlinedIcon /></IconButton>
<IconButton onClick={(e) => toggleTagDialog(e, true)} size="small"
style={{display: "inline-block", float: "right", padding: "0px"}}>
<LocalOfferOutlinedIcon />
</IconButton>
{openTagDialog &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openTagDialog}
onClose={(e)=>{toggleTagDialog(e, false)}}
@@ -15,7 +15,7 @@ export function MythicTextEditDialog(props) {
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">{props.title}</DialogTitle>
<DialogContent dividers={true}>
<DialogContent dividers={true} style={{padding: 0}}>
<AceEditor
mode="json"
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
@@ -24,6 +24,8 @@ export function MythicTextEditDialog(props) {
showGutter={true}
highlightActiveLine={true}
value={props.value}
showPrintMargin={false}
wrapEnabled={true}
focus={true}
width={"100%"}
setOptions={{
@@ -1,111 +1,133 @@
import React from 'react';
import { styled } from '@mui/material/styles';
import PropTypes from 'prop-types';
import {TextField} from '@mui/material';
import {useDebounce} from "../utilities/useDebounce";
const PREFIX = 'MythicTextField';
const classes = {
root: `${PREFIX}-root`
root: `${PREFIX}Div-root`,
textFieldRoot: `${PREFIX}-root`
};
const Root = styled('div')({
const Root = styled('div')(({theme}) => ({
[`&.${classes.root}`]: {
'& input:valid + fieldset': {
borderColor: 'grey',
borderWidth: 1,
},
}));
const ValidationTextField = styled(TextField)(({theme}) => ({
[`&.${classes.textFieldRoot}`]: {
'& fieldset': {
borderColor: 'grey',
borderWidth: 1,
},
'& input:invalid + fieldset': {
borderColor: 'red',
borderWidth: 2,
borderColor: 'red',
borderWidth: 2,
},
'& input:valid:focus + fieldset': {
borderLeftWidth: 6,
padding: '4px !important', // override inline-style
borderLeftWidth: 6,
padding: '4px !important', // override inline-style
},
},
});
'& textarea:focus + textarea + fieldset': {
borderLeftWidth: 6,
//padding: '4px !important', // override inline-style
},
},
}));
const ValidationTextField = TextField;
class MythicTextField extends React.Component {
static propTypes = {
placeholder: PropTypes.string,
name: PropTypes.string,
validate: PropTypes.func,
width: PropTypes.number,
onChange: PropTypes.func.isRequired,
requiredValue: PropTypes.bool,
type: PropTypes.string,
onEnter: PropTypes.func,
autoFocus: PropTypes.bool,
autoComplete: PropTypes.bool,
showLabel: PropTypes.bool,
variant: PropTypes.string,
inline: PropTypes.bool,
marginBottom: PropTypes.string,
value: PropTypes.any,
}
onChange = evt => {
const name = this.props.name;
const value = evt.target.value;
const error = this.props.validate ? this.props.validate(value) : false;
this.props.onChange(name, value, error, evt);
}
checkError = () => {
return this.props.validate ? this.props.validate(this.props.value) : false
}
onKeyPress = (event) => {
const MythicTextField = ({
placeholder,
name,
validate,
width,
onChange,
requiredValue=false,
type = "text",
onEnter,
autoFocus,
autoComplete=false,
showLabel = true,
variant = "outlined",
inline,
marginBottom = "5px",
value,
disabled = false,
marginTop = "5px",
InputProps = {},
inputLabelProps = {},
multiline = false,
maxRows = 10,
errorText = "",
helperText = "",
debounceDelay = 100,
}) => {
const [localValue, setLocalValue] = React.useState({value: value, event: null});
const [localError, setLocalError] = React.useState(false);
const debouncedLocalInput = useDebounce(localValue, debounceDelay);
React.useEffect( () => {
const error = validate ? validate(debouncedLocalInput.value) : false;
onChange(name, debouncedLocalInput.value, error, debouncedLocalInput.event);
}, [debouncedLocalInput]);
React.useEffect( () => {
setLocalValue({value: value, event: null});
}, [value]);
const handleChange = (evt) => {
const newValue = evt.target.value;
// Update local state immediately for responsive UI
setLocalValue({value: newValue, event: evt});
setLocalError(validate ? validate(newValue) : false);
};
const onKeyPress = (event) => {
if(event.key === "Enter") {
if(event.shiftKey){
this.onChange(event);
handleChange(event);
return;
}
if (this.props.onEnter !== undefined) {
if (onEnter !== undefined) {
event.stopPropagation();
event.preventDefault();
this.props.onEnter(event);
onEnter(event);
}
}else{
this.onChange(event);
handleChange(event);
}
}
render(){
return (
<Root style={{width: this.props.width ? this.props.width + "rem" : "100%", display: this.props.inline ? "inline-block": "",}}>
<ValidationTextField
fullWidth={true}
placeholder={this.props.placeholder}
value={this.props.value}
onChange={this.onChange}
color={"secondary"}
onKeyDown={this.onKeyPress}
label={this.props.showLabel === undefined ? this.props.name : this.props.showLabel ? this.props.name : undefined}
autoFocus={this.props.autoFocus}
variant={this.props.variant === undefined ? "outlined" : this.props.variant}
data-lpignore={true}
autoComplete={this.props.autoComplete === undefined ? "new-password" : (this.props.autoComplete ? "on" : "new-password")}
disabled={this.props.disabled === undefined ? false : this.props.disabled}
required={this.props.requiredValue ? this.props.requiredValue : false}
InputLabelProps={this.props.inputLabelProps}
multiline={this.props.multiline ? this.props.multiline : false}
maxRows={this.props.maxRows}
error={this.checkError()}
type={this.props.type === undefined ? "text" : this.props.type}
onWheel={ event => event.target.blur() }
InputProps={{...this?.props?.InputProps, spellCheck: false}}
helperText={this.checkError() ? this.props.errorText : this.props.helperText}
style={{
padding:0,
paddingLeft: "5px",
marginBottom: this.props.marginBottom ? this.props.marginBottom : "10px",
display: this.props.inline ? "inline-block": "",
}}
classes={{
root: classes.root
}} />
</Root>
);
}
return (
<Root style={{width: width ? width + "rem" : "100%", display: inline ? "inline-block": "",}}>
<ValidationTextField
fullWidth={true}
placeholder={placeholder}
value={localValue.value}
onChange={handleChange}
color={"secondary"}
onKeyDown={onKeyPress}
label={showLabel ? name : undefined}
autoFocus={autoFocus}
variant={variant}
data-lpignore={true}
autoComplete={autoComplete === undefined ? "new-password" : (autoComplete ? "on" : "off")}
disabled={disabled}
required={requiredValue}
InputLabelProps={inputLabelProps}
multiline={multiline}
maxRows={maxRows}
error={localError}
type={type}
onWheel={ event => event.target.blur() }
InputProps={{...InputProps, spellCheck: false}}
helperText={localError ? errorText : helperText}
style={{
padding:0,
marginBottom: marginBottom ? marginBottom : "5px",
marginTop: marginTop ? marginTop: "5px",
display: inline ? "inline-block": "",
}}
classes={{
root: classes.textFieldRoot
}} />
</Root>
);
}
export default MythicTextField;
-653
View File
@@ -1,653 +0,0 @@
import React from 'react';
import { styled } from '@mui/material/styles';
import clsx from 'clsx';
import { useTheme } from '@mui/material/styles';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import IconButton from '@mui/material/IconButton';
import MenuIcon from '@mui/icons-material/Menu';
import MenuItem from '@mui/material/MenuItem';
import ManageAccountsTwoToneIcon from '@mui/icons-material/ManageAccountsTwoTone';
import Menu from '@mui/material/Menu';
import { Link } from 'react-router-dom';
import Drawer from '@mui/material/Drawer';
import List from '@mui/material/List';
import Divider from '@mui/material/Divider';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import ListItem from '@mui/material/ListItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import { useReactiveVar } from '@apollo/client';
import { menuOpen, FailedRefresh } from '../cache';
import { TopAppBarEventLogNotifications } from './TopAppBarEventLogNotifications';
import { EventFeedNotifications } from './EventFeedNotifications';
import HelpTwoToneIcon from '@mui/icons-material/HelpTwoTone';
import PhoneCallbackIcon from '@mui/icons-material/PhoneCallback';
import {ReactComponent as ReactLogo} from './mythic_red_small.svg';
import SpaceDashboardTwoToneIcon from '@mui/icons-material/SpaceDashboardTwoTone';
import ListSubheader from '@mui/material/ListSubheader';
import Collapse from '@mui/material/Collapse';
import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore';
import LayersTwoToneIcon from '@mui/icons-material/LayersTwoTone';
import TableChartTwoToneIcon from '@mui/icons-material/TableChartTwoTone';
import PostAddIcon from '@mui/icons-material/PostAdd';
import SupervisorAccountIcon from '@mui/icons-material/SupervisorAccount';
import EditIcon from '@mui/icons-material/Edit';
import { Typography } from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import BarChartIcon from '@mui/icons-material/BarChart';
import HeadsetTwoToneIcon from '@mui/icons-material/HeadsetTwoTone';
import CodeIcon from '@mui/icons-material/Code';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faBiohazard, faFlagCheckered} from '@fortawesome/free-solid-svg-icons';
import AttachmentIcon from '@mui/icons-material/Attachment';
import FingerprintIcon from '@mui/icons-material/Fingerprint';
import {faSocks} from '@fortawesome/free-solid-svg-icons';
import CameraAltTwoToneIcon from '@mui/icons-material/CameraAltTwoTone';
import {mythicUIVersion} from '../index';
import {MythicStyledTooltip} from './MythicComponents/MythicStyledTooltip';
import VpnKeyIcon from '@mui/icons-material/VpnKey';
import ThumbDownTwoTone from '@mui/icons-material/ThumbDownTwoTone';
import { MythicDialog } from './MythicComponents/MythicDialog';
import {MythicFeedbackDialog} from './MythicComponents/MythicFeedbackDialog';
import LocalOfferTwoToneIcon from '@mui/icons-material/LocalOfferTwoTone';
import StorageIcon from '@mui/icons-material/Storage';
import PublicIcon from '@mui/icons-material/Public';
import LightModeTwoToneIcon from '@mui/icons-material/LightModeTwoTone';
import DarkModeTwoToneIcon from '@mui/icons-material/DarkModeTwoTone';
import PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwoTone';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import {TopAppBarEventingNotifications} from "./TopAppBarEventingNotifications";
import {useQuery, gql} from '@apollo/client';
import VerifiedTwoToneIcon from '@mui/icons-material/VerifiedTwoTone';
const PREFIX = 'TopAppBar';
const classes = {
root: `${PREFIX}-root`,
title: `${PREFIX}-title`,
hide: `${PREFIX}-hide`,
drawer: `${PREFIX}-drawer`,
drawerPaper: `${PREFIX}-drawerPaper`,
drawerHeader: `${PREFIX}-drawerHeader`,
listSubHeader: `${PREFIX}-listSubHeader`,
appBar: `${PREFIX}-appBar`,
appBarShift: `${PREFIX}-appBarShift`,
nested: `${PREFIX}-nested`,
mythicElement: `${PREFIX}-mythicElement`,
menuButton: `${PREFIX}-menuButton`,
};
const StyledAppBar = styled(AppBar)((
{
theme
}
) => ({
[`& .${classes.hide}`]: {
display: 'none',
},
[`& .${classes.menuButton}`]: {
display: 'inline-block',
},
[`&.${classes.appBar}`]: {
width: "100%",
backgroundColor: theme.topAppBarColor,
transition: theme.transitions.create(['margin', 'width'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
borderTopRightRadius: theme.palette.mode === "dark" ? "3px" : 0,
borderTopLeftRadius: theme.palette.mode === "dark" ? "3px" : 0,
position: "sticky",
overflow: "hidden",
maxHeight: "48px"
},
[`&.${classes.appBarShift}`]: {
maxWidth: `calc(100% - ${drawerWidth}px)`,
marginLeft: drawerWidth,
transition: theme.transitions.create(['margin', 'width'], {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
borderTopRightRadius: theme.palette.mode === "dark" ? "3px" : 0,
borderTopLeftRadius: theme.palette.mode === "dark" ? "3px" : 0,
},
}));
const StyledDrawer = styled(Drawer)((
{
theme
}
) => ({
[`& .${classes.title}`]: {
flexGrow: 1,
},
[`&.${classes.drawer}`]: {
width: drawerWidth,
flexShrink: 0,
},
[`& .${classes.drawerPaper}`]: {
width: drawerWidth,
},
[`& .${classes.drawerHeader}`]: {
display: 'flex',
alignItems: 'center',
padding: theme.spacing(0, 1),
justifyContent: 'flex-end',
},
[`& .${classes.listSubHeader}`]: {
backgroundColor: theme.palette.listSubHeader.default,
borderTopLeftRadius: "3px",
borderTopRightRadius: "3px",
},
[`& .${classes.nested}`]: {
paddingLeft: theme.spacing(4),
},
}));
const drawerWidth = 240;
const GET_SETTINGS = gql`
query getGlobalSettings {
getGlobalSettings {
settings
}
}
`;
export function TopAppBar(props) {
const theme = useTheme();
const me = props.me;
const isOpen = useReactiveVar(menuOpen);
const [openGlobal, setOpenGlobal] = React.useState(false);
const [openCreate, setOpenCreate] = React.useState(false);
const [openOperations, setOpenOperations] = React.useState(false);
const [openData, setOpenData] = React.useState(false);
const [serverVersion, setServerVersion] = React.useState("...");
const [serverName, setServerName] = React.useState("...");
useQuery(GET_SETTINGS, {fetchPolicy: "no-cache",
onCompleted: (data) => {
setServerVersion(data.getGlobalSettings.settings["MYTHIC_SERVER_VERSION"]);
setServerName(data.getGlobalSettings.settings["MYTHIC_GLOBAL_SERVER_NAME"]);
}
});
const handleDrawerOpen = () => {
menuOpen(true);
};
const handleDrawerClose = () => {
menuOpen(false);
};
const handleToggleGlobal = () => {
setOpenGlobal(!openGlobal);
}
const handleToggleCreate = () => {
setOpenCreate(!openCreate);
}
const handleToggleOperations = () => {
setOpenOperations(!openOperations);
}
const handleToggleData = () => {
setOpenData(!openData);
}
return (
<>
{me?.user?.current_operation_id ? (<EventFeedNotifications me={me} />) : null }
<StyledAppBar className={clsx(classes.appBar, {[classes.appBarShift]: isOpen,})}>
{ me?.loggedIn ? (
<Toolbar variant="dense" style={{width: "100%"}} >
<IconButton
edge="start"
className={clsx(isOpen && classes.hide, !isOpen && classes.menuButton)}
color="inherit"
aria-label="menu"
disableFocusRipple={true}
disableRipple={true}
onClick={handleDrawerOpen}
>
<MenuIcon fontSize={"large"} />
</IconButton>
<span style={{width: "100%"}}>
<span className={'hideOnSmallWidth'}>
<TopBarShortcuts />
{me?.user?.current_operation_id === 0 ? (
<Link style={{display: "inline-flex", alignItems: "center", paddingRight: "10px", color: "#f84d4d",
fontWeight: "bold",}} to="/new/operations">
{"CLICK HERE TO SET OPERATION!"}
</Link>
) : (
<>
<Link style={{paddingRight: "10px", color: "white", textDecoration: "none", fontSize: "1em"}} to="/new/operations">
{me?.user?.current_operation}
{me?.user?.current_operation_complete &&
<IconButton disabled>
<VerifiedTwoToneIcon style={{padding: 0, color: "white"}} />
</IconButton>
}
</Link>
</>
)}
</span>
</span>
<TopBarRightShortcuts me={me} toggleTheme={props.toggleTheme} serverName={serverName} />
</Toolbar>
) : null
}
</StyledAppBar>
<StyledDrawer
className={classes.drawer}
anchor="left"
open={isOpen}
classes={{
paper: classes.drawerPaper,
}}
onClose={handleDrawerClose}
>
<div className={classes.drawerHeader} role="presentation">
<ReactLogo style={{width: "80%", display: "inline-block"}}/>
<IconButton onClick={handleDrawerClose} size="large">
{theme.direction === 'ltr' ? <ChevronLeftIcon className="mythicElement"/> : <ChevronRightIcon className="mythicElement"/>}
</IconButton>
</div>
<List
subheader={
<ListSubheader className={classes.listSubHeader} component="div" id="nested-list-subheader">
Home
</ListSubheader>
}>
<div style={{marginLeft: "15px"}}>
<b>Mythic Version:</b> v{serverVersion}<br/>
<b>UI Version:</b> v{mythicUIVersion}<br/>
</div>
<ListItem button component={Link} to='/new' key={"home"} onClick={handleDrawerClose}>
<ListItemIcon ><SpaceDashboardTwoToneIcon fontSize={"large"} className="mythicElement" /></ListItemIcon>
<ListItemText primary={"Dashboard"} />
</ListItem>
</List>
<List
subheader={
<ListSubheader className={classes.listSubHeader} component="div" id="nested-list-subheader">
Global Configurations
</ListSubheader>
}>
<ListItem button onClick={handleToggleGlobal}>
<ListItemIcon><LayersTwoToneIcon fontSize={"large"} /></ListItemIcon>
<ListItemText>Services</ListItemText>
{openGlobal ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={openGlobal} unmountOnExit >
<List component="div" disablePadding style={{border: 0}}>
<ListItem button className={classes.nested} target="_blank" component={Link} to='/jupyter' key={"jupyter"} onClick={handleDrawerClose}>
<ListItemIcon><CodeIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Jupyter Notebooks"} />
</ListItem>
<ListItem button className={classes.nested} target="_blank" component={Link} to='/console' key={"console"} onClick={handleDrawerClose}>
<ListItemIcon><StorageIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"GraphQL Console"} />
</ListItem>
<ListItem button className={classes.nested} component={Link} to='/new/consuming_services' key={"consuming"} onClick={handleDrawerClose}>
<ListItemIcon><PublicIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Consuming Services"} />
</ListItem>
</List>
</Collapse>
<ListItem button onClick={handleToggleCreate}>
<ListItemIcon><PostAddIcon /></ListItemIcon>
<ListItemText>Create</ListItemText>
{openCreate ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={openCreate} unmountOnExit>
<List component="div" disablePadding style={{border: 0}}>
<ListItem button className={classes.nested} component={Link} to='/new/createpayload' key={"createpayload"} onClick={handleDrawerClose} state={{from: 'TopAppBar'}}>
<ListItemIcon><PostAddIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Create Payload"} />
</ListItem>
<ListItem button className={classes.nested} component={Link} to='/new/createwrapper' key={"createwrapper"} onClick={handleDrawerClose}>
<ListItemIcon><PostAddIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Create Wrapper"} />
</ListItem>
</List>
</Collapse>
<ListItem button onClick={handleToggleOperations}>
<ListItemIcon><SupervisorAccountIcon fontSize={"large"} /></ListItemIcon>
<ListItemText>Operation Config</ListItemText>
{openOperations ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={openOperations} unmountOnExit>
<List component="div" disablePadding style={{border: 0}}>
<ListItem button className={classes.nested} component={Link} to='/new/payloadtypes' key={"payloadtypes"} onClick={handleDrawerClose}>
<ListItemIcon><HeadsetTwoToneIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Agents & C2"} />
</ListItem>
<ListItem button className={classes.nested} component={Link} to='/new/operations' key={"modifyoperations"} onClick={handleDrawerClose}>
<ListItemIcon><EditIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Modify Operations"} />
</ListItem>
<ListItem button className={classes.nested} component={Link} to='/new/browserscripts' key={"browserscripts"} onClick={handleDrawerClose}>
<ListItemIcon><CodeIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"BrowserScripts"} />
</ListItem>
</List>
</Collapse>
</List>
<List
subheader={
<ListSubheader className={classes.listSubHeader} component="div" id="nested-list-subheader">
Operational Views
</ListSubheader>
}>
<ListItem button onClick={handleToggleData}>
<ListItemIcon><BarChartIcon fontSize={"large"} /></ListItemIcon>
<ListItemText>Operational Data</ListItemText>
{openData ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={openData} unmountOnExit>
<List component="div" disablePadding style={{border: 0}}>
<ListItem button className={classes.nested} component={Link} to='/new/payloads' key={"payloads"} onClick={handleDrawerClose}>
<ListItemIcon><FontAwesomeIcon icon={faBiohazard} size="2x"/></ListItemIcon>
<ListItemText primary={"Payloads"} />
</ListItem>
<ListItem button className={classes.nested} component={Link} to='/new/search?tab=callbacks&searchField=Host&search=' key={"search"} onClick={handleDrawerClose}>
<ListItemIcon><SearchIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Search"} />
</ListItem>
<ListItem button className={classes.nested} component={Link} to='/new/mitre' onClick={handleDrawerClose}>
<ListItemIcon><TableChartTwoToneIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"ATT&CK"} />
</ListItem>
<ListItem button className={classes.nested} component={Link} to='/new/reporting' onClick={handleDrawerClose}>
<ListItemIcon><FontAwesomeIcon size={"2x"} icon={faFlagCheckered} /></ListItemIcon>
<ListItemText primary={"Reporting"} />
</ListItem>
<ListItem button className={classes.nested} component={Link} to='/new/tagtypes' onClick={handleDrawerClose}>
<ListItemIcon><LocalOfferTwoToneIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Tags"} />
</ListItem>
<ListItem button className={classes.nested} component={Link} to='/new/eventing' onClick={handleDrawerClose}>
<ListItemIcon><PlayCircleFilledTwoToneIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Eventing"} />
</ListItem>
</List>
</Collapse>
<ListItem button component={Link} to='/new/callbacks' key={"callbacks"} onClick={handleDrawerClose}>
<ListItemIcon><PhoneCallbackIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Active Callbacks"} />
</ListItem>
</List>
<Divider />
</StyledDrawer>
{me?.user?.current_operation_banner_text !== "" &&
<Typography style={{backgroundColor: me?.user?.current_operation_banner_color,
width: "100%", textAlign: "center", fontWeight: "600", color: "white", borderRadius: "4px", border: "1px solid grey"}} >
{me?.user?.current_operation_banner_text}
</Typography>
}
</>
);
}
function TopBarShortcuts({}){
return (
<>
<MythicStyledTooltip title="C2 Profiles and Payload Types" arrow >
<IconButton component={Link} to='/new/payloadtypes'
color="inherit"
size="large" disableFocusRipple={true}
disableRipple={true}>
<HeadsetTwoToneIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Payloads" arrow >
<IconButton component={Link} to='/new/payloads' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<FontAwesomeIcon size={"lg"} icon={faBiohazard} />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Search Operation" arrow >
<IconButton component={Link} to='/new/search?tab=callbacks&searchField=Host&search=' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<SearchIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Files" arrow >
<IconButton
component={Link}
to='/new/search?searchField=Filename&tab=files&location=Downloads'
color="inherit"
disableFocusRipple={true}
disableRipple={true}>
<AttachmentIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Artifacts" arrow >
<IconButton
disableFocusRipple={true}
disableRipple={true}
component={Link}
to='/new/search?searchField=Artifact&tab=artifacts'
color="inherit">
<FingerprintIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Proxies" arrow >
<IconButton component={Link} to='/new/search?tab=socks' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<FontAwesomeIcon size={"lg"} icon={faSocks} />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Screenshots" arrow >
<IconButton
component={Link}
to='/new/search?searchField=Filename&tab=files&location=Screenshots'
color="inherit"
disableFocusRipple={true}
disableRipple={true}
>
<CameraAltTwoToneIcon fontSize={"large"} />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Credentials" arrow >
<IconButton
component={Link}
to='/new/search?searchField=Account&tab=credentials'
color="inherit"
disableFocusRipple={true}
disableRipple={true}>
<VpnKeyIcon fontSize={"large"} className="mythicElement" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Active Callbacks" arrow >
<IconButton component={Link} to='/new/callbacks' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<PhoneCallbackIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Reporting" arrow >
<IconButton component={Link} to='/new/reporting' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<FontAwesomeIcon size={"lg"} icon={faFlagCheckered} />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="MITRE ATT&CK" arrow >
<IconButton component={Link} to='/new/mitre' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<TableChartTwoToneIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Operation Tags" arrow >
<IconButton component={Link} to='/new/tagtypes' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<LocalOfferTwoToneIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Eventing Scripts" arrow >
<TopAppBarEventingNotifications />
</MythicStyledTooltip>
</>
)
}
function TopBarRightShortcuts({me, toggleTheme, serverName}){
const theme = useTheme();
const feedbackRef = React.useRef(null);
const settingsRef = React.useRef(null);
const documentationRef = React.useRef(null);
const [anchorEl, setAnchorEl] = React.useState(null);
const [documentationAnchorEl, setDocumentationAnchorEl] = React.useState(null);
const [openFeedbackForm, setOpenFeedbackForm] = React.useState(false);
const handleLogout = () => {
menuOpen(false);
FailedRefresh();
}
const handleMenu = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = (evt) => {
setAnchorEl(null);
};
const handleDocumentationMenu = (event) => {
setDocumentationAnchorEl(event.currentTarget);
};
const handleDocumentationClose = (evt) => {
setDocumentationAnchorEl(null);
};
return (
<>
<Menu
id="menu-appbar"
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
open={Boolean(anchorEl)}
onClose={handleClose}
>
<MenuItem disabled divider={true} style={{display: "block"}}>
<Typography paragraph={true} variant={"caption"} style={{marginBottom: "0"}}>Server Name:</Typography>
<Typography paragraph={true} variant={"body1"} style={{marginBottom: "0", fontWeight: 600}}>{serverName}</Typography>
</MenuItem>
<MenuItem divider={true} style={{display: "block"}} component={Link} to="/new/settings" onClick={handleClose} name="settings">
<Typography paragraph={true} variant="caption" style={{marginBottom: "0"}}>Signed in as:</Typography>
<Typography paragraph={true} variant="body1" style={{marginBottom: "0", fontWeight: 600}}> {me?.user?.username || "" } </Typography>
</MenuItem>
<MenuItem onClick={handleLogout}>Logout</MenuItem>
</Menu>
<div style={{display: "inline-flex", justifyContent: "flex-end", float: "right"}}>
<IconButton
aria-label="feedback"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={() => setOpenFeedbackForm(true)}
ref={feedbackRef}
color="inherit"
style={{float: "right", }}
disableFocusRipple={true}
disableRipple={true}>
<MythicStyledTooltip title="Send Support Feedback">
<ThumbDownTwoTone fontSize={"large"} className="mythicElement" />
</MythicStyledTooltip>
</IconButton>
{openFeedbackForm &&
<MythicDialog fullWidth={true} maxWidth="md" open={openFeedbackForm}
onClose={()=>{setOpenFeedbackForm(false);}}
innerDialog={<MythicFeedbackDialog
title={"Submit Feedback via Webhook"}
onClose={()=>{setOpenFeedbackForm(false);}} />}
/>
}
<IconButton
aria-label="documentation links"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={handleDocumentationMenu}
ref={documentationRef}
color="inherit"
style={{float:"right"}}
disableFocusRipple={true}
disableRipple={true}>
<MythicStyledTooltip title="Help">
<HelpTwoToneIcon fontSize={"large"} className="mythicElement"/>
<KeyboardArrowDownIcon />
</MythicStyledTooltip>
</IconButton>
<Menu
id="menu-appbar"
anchorEl={documentationAnchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
open={Boolean(documentationAnchorEl)}
onClose={handleDocumentationClose}
>
<MenuItem component={Link} target="_blank" to="/docs/agents" onClick={handleDocumentationClose}>Agent Documentation</MenuItem>
<MenuItem component={Link} target="_blank" to="/docs/wrappers" onClick={handleDocumentationClose}>Wrapper Documentation</MenuItem>
<MenuItem component={Link} target="_blank" to="/docs/c2-profiles" onClick={handleDocumentationClose}>C2 Profile Documentation</MenuItem>
<MenuItem component={Link} to={{pathname: "https://docs.mythic-c2.net"}} target="_blank" onClick={handleDocumentationClose}>Mythic Documentation</MenuItem>
</Menu>
<TopAppBarEventLogNotifications />
<IconButton
aria-label="account of current user"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={handleMenu}
ref={settingsRef}
color="inherit"
style={{float: "right", }}
disableFocusRipple={true}
disableRipple={true} >
<MythicStyledTooltip title="Settings or Logout">
<ManageAccountsTwoToneIcon fontSize={"large"} className="mythicElement" />
<KeyboardArrowDownIcon />
</MythicStyledTooltip>
</IconButton>
{theme.palette.mode === 'dark' &&
<IconButton
disableFocusRipple={true}
disableRipple={true}
onClick={toggleTheme}
style={{float: "right", paddingRight: "0px", color: '#2f0e67'}}
>
<MythicStyledTooltip title="Change to Light Mode">
<DarkModeTwoToneIcon fontSize={"large"} className="mythicElement" />
</MythicStyledTooltip>
</IconButton>
}
{theme.palette.mode === 'light' &&
<IconButton
disableFocusRipple={true}
disableRipple={true}
onClick={toggleTheme}
style={{float: "right", paddingRight: "0px", color: '#eacc1b'}}
>
<MythicStyledTooltip title="Change to Dark Mode">
<LightModeTwoToneIcon fontSize={"large"} className="mythicElement" />
</MythicStyledTooltip>
</IconButton>
}
</div>
</>
)
}
@@ -2,10 +2,8 @@ import React from 'react';
import {useSubscription, gql } from '@apollo/client';
import Badge from '@mui/material/Badge';
import NotificationsActiveTwoToneIcon from '@mui/icons-material/NotificationsActiveTwoTone';
import { Link } from 'react-router-dom';
import { IconButton } from '@mui/material';
import {snackActions} from './utilities/Snackbar';
import {MythicStyledTooltip} from "./MythicComponents/MythicStyledTooltip";
import { useTheme } from '@mui/material/styles';
import {alertCount} from "../cache";
const SUB_Event_Logs = gql`
subscription OperationAlertCounts{
@@ -16,37 +14,36 @@ subscription OperationAlertCounts{
}
`;
export function TopAppBarEventLogNotifications(props) {
const { loading, error, data } = useSubscription(SUB_Event_Logs, {
onError: data => {
snackActions.error("Mythic encountered an error getting event log messages: " + data.toString());
console.error(data);
}
});
export function TopAppBarVerticalEventLogNotifications(props) {
const theme = useTheme();
const [alerts, setAlerts] = React.useState(alertCount());
const { loading, error } = useSubscription(SUB_Event_Logs, {
onError: data => {
console.log("vertical event log error")
console.error(data);
},
onData: ({data}) => {
const newAlertCount = data.data.operation_stream[0].alert_count;
if(newAlertCount !== alerts){
setAlerts(newAlertCount);
alertCount(newAlertCount);
}
}
});
return (
<MythicStyledTooltip title="Event Feed">
<IconButton
color="inherit"
component={Link}
disableFocusRipple={true}
disableRipple={true}
to='/new/EventFeed'
style={{float: "right"}}>
{error ? (
<Badge color="secondary" badgeContent={0}>
<NotificationsActiveTwoToneIcon fontSize={"large"} />
</Badge>
) : (
<Badge badgeContent={data?.operation_stream[0]?.alert_count || 0}
color="error" max={99}
sx={{marginTop: "3px"}}
>
<NotificationsActiveTwoToneIcon fontSize={"large"} />
</Badge>
)}
</IconButton>
</MythicStyledTooltip>
error ? (
<Badge color="secondary" badgeContent={"X"}>
<NotificationsActiveTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} />
</Badge>
) : (
<Badge badgeContent={alerts}
color="error" max={99}
>
<NotificationsActiveTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"}/>
</Badge>
)
);
}
@@ -1,65 +0,0 @@
import React from 'react';
import {useSubscription, gql } from '@apollo/client';
import Badge from '@mui/material/Badge';
import PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwoTone';
import { Link } from 'react-router-dom';
import { IconButton } from '@mui/material';
import {snackActions} from './utilities/Snackbar';
import {MythicStyledTooltip} from "./MythicComponents/MythicStyledTooltip";
const OperationEventingCounts = gql`
subscription OperationEventingCounts {
eventgroupinstance_stream(cursor: {initial_value: {updated_at: "1970-01-01"}, ordering: ASC}, batch_size: 50) {
id
end_timestamp
}
}
`;
export function TopAppBarEventingNotifications(props) {
const [totalRunning, setTotalRunning] = React.useState([]);
const { loading, error, data } = useSubscription(OperationEventingCounts, {
onData: ({data}) => {
const newRunningCounts = data.data.eventgroupinstance_stream.reduce( (prev, cur) => {
let existingIndex = prev.findIndex( e => e.id === cur.id);
if(existingIndex >= 0){
if(cur.end_timestamp !== null){
prev.splice(existingIndex, 1);
}
return [...prev];
}
if(cur.end_timestamp === null){
return [...prev, cur];
}
return [...prev];
}, totalRunning);
setTotalRunning(newRunningCounts);
},
onError: data => {
snackActions.error("Mythic encountered an error getting eventing counts: " + data.toString());
console.error(data);
}
});
return (
<MythicStyledTooltip title="Eventing Counts">
<IconButton
color="inherit"
component={Link}
disableFocusRipple={true}
disableRipple={true}
to='/new/Eventing'>
{error ? (
<Badge color="secondary" badgeContent={0}>
<PlayCircleFilledTwoToneIcon fontSize={"large"} />
</Badge>
) : (
<Badge badgeContent={totalRunning.length} color="warning">
<PlayCircleFilledTwoToneIcon fontSize={"large"} />
</Badge>
)}
</IconButton>
</MythicStyledTooltip>
);
}
File diff suppressed because it is too large. Load diff
@@ -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>
);
}
@@ -9,33 +9,32 @@ import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import { BrowserScriptsTableRow } from './BrowserScriptsTableRow';
import {useTheme} from '@mui/material/styles';
import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined';
import 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,
marginBottom: "5px", marginLeft: "5px", marginRight: "5px"}} variant={"elevation"}>
<Typography variant="h3" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
Browser Scripts
</Typography>
<Button size="small" onClick={() => setOpenNewScriptDialog(true)} style={{float: "right", marginTop: "10px", marginRight: "30px"}} startIcon={<AddCircleOutlineOutlinedIcon/>} color="primary" variant="contained">New Script</Button>
{openNewScriptDialog ? (
<>
<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 &&
<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>
@@ -59,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!) {
@@ -45,21 +46,46 @@ query GetCallbackDetails($callback_id: Int!) {
}
}
`;
const CustomListElement = React.memo(({valueObj, handleToggle}) => {
return (
<ListItem style={{padding: 0}} key={valueObj.display_value} role="listitem" button
onClick={() => handleToggle(valueObj)}>
<ListItemIcon>
<Checkbox
checked={valueObj.checked}
disableRipple
inputProps={{'aria-labelledby': valueObj.label}}
/>
</ListItemIcon>
<ListItemText id={valueObj.label} primary={valueObj.display_value}/>
</ListItem>
)
});
const CustomList = ({title, items, handleToggle}) => {
const renderedList = React.useMemo( () => {
return items.map((valueObj) => (
<CustomListElement key={valueObj.display_value} valueObj={valueObj} handleToggle={handleToggle} />
))
}, [items, handleToggle]);
return (
<>
<CardHeader className={classes.cardHeader} title={title}/>
<StyledDivider classes={{root: classes.divider}}/>
<CardContent style={{flexGrow: 1, overflowY: "auto", padding: 0}}>
<List dense component="div" role="list" style={{padding: 0, width: "100%"}}>
{renderedList}
</List>
</CardContent>
</>
)
};
export function AddRemoveCallbackCommandsDialog(props) {
const [checked, setChecked] = React.useState([]);
const [left, setLeft] = React.useState([]);
const [originalLeft, setOriginalLeft] = React.useState([]);
const [originalRight, setOriginalRight] = React.useState([]);
const [right, setRight] = React.useState([]);
const [leftTitle, setLeftTitle] = React.useState("Commands Not Included");
const [rightTitle, setRightTitle] = React.useState("Commands Included");
const leftChecked = intersection(checked, left);
const rightChecked = intersection(checked, right);
const compareElements = (a, b) => {
return a.cmd === b.cmd && a.payloadtype.name === b.payloadtype.name;
}
const leftTitle = "Commands Not Included";
const rightTitle = "Commands Included";
useQuery(getCommandsQuery, {variables: {callback_id: props.callback_id},
fetchPolicy: "no-cache",
onCompleted: (data) => {
@@ -67,10 +93,9 @@ export function AddRemoveCallbackCommandsDialog(props) {
return {...c, payloadtype: {name: data.callback_by_pk.payload.payloadtype.name}}
})
originalLeftFromQuery = [...originalLeftFromQuery, ...data.command];
setOriginalLeft(originalLeftFromQuery);
setOriginalRight(data.callback_by_pk.loadedcommands);
const leftData = originalLeftFromQuery.reduce( (prev, cur) => {
let leftData = originalLeftFromQuery.reduce( (prev, cur) => {
if( data.callback_by_pk.loadedcommands.filter(c => c.command.cmd === cur.cmd && c.command.payloadtype.name === cur.payloadtype.name).length === 0){
return [...prev, cur];
} else {
@@ -79,35 +104,46 @@ export function AddRemoveCallbackCommandsDialog(props) {
}, []);
leftData.sort( (a,b) => a.cmd < b.cmd ? -1 : 1);
leftData = leftData.map( c => {return {...c,
checked: false,
display_value: c.cmd + " (" + c?.payloadtype?.name + ")",
label: `transfer-list-item-${c.cmd + " (" + c?.payloadtype?.name + ")"}-label`
}});
setLeft(leftData);
const rightData = data.callback_by_pk.loadedcommands.map( c => c.command);
let rightData = data.callback_by_pk.loadedcommands.map( c => c.command);
rightData.sort( (a,b) => a.cmd < b.cmd ? -1 : 1);
rightData = rightData.map( c => {return {...c,
checked: false,
display_value: c.cmd + " (" + c?.payloadtype?.name + ")",
label: `transfer-list-item-${c.cmd + " (" + c?.payloadtype?.name + ")"}-label`
}});
setRight(rightData);
},
onError: (data) => {
}
})
function not(a, b) {
return a.filter((value) => b.indexOf(value) === -1);
}
function intersection(a, b) {
return a.filter((value) => b.indexOf(value) !== -1);
}
const handleToggle = (value) => () => {
let currentIndex = -1;
currentIndex = checked.indexOf(value);
const newChecked = [...checked];
if (currentIndex === -1) {
newChecked.push(value);
} else {
newChecked.splice(currentIndex, 1);
const handleToggle = (value) => {
let found = false;
const newLeft = left.map( c => {
if(c.display_value === value.display_value){
c.checked = !c.checked;
found = true;
}
return c;
});
if(found){
setLeft(newLeft);
return
}
setChecked(newChecked);
const newRight = right.map( c => {
if(c.display_value === value.display_value){
c.checked = !c.checked;
}
return c;
});
setRight(newRight);
};
const handleAllRight = () => {
@@ -116,49 +152,26 @@ export function AddRemoveCallbackCommandsDialog(props) {
};
const handleCheckedRight = () => {
setRight(right.concat(leftChecked));
setLeft(not(left, leftChecked));
setChecked(not(checked, leftChecked));
let leftChecked = left.filter(c => c.checked);
leftChecked = leftChecked.map(c => {return {...c, checked: false}})
let leftNotChecked = left.filter(c => !c.checked);
setRight(right.concat(leftChecked));
setLeft(leftNotChecked);
};
const handleCheckedLeft = () => {
setLeft(left.concat(rightChecked));
setRight(not(right, rightChecked));
setChecked(not(checked, rightChecked));
let rightChecked = right.filter(c => c.checked);
rightChecked = rightChecked.map(c => {return {...c, checked: false}})
let rightNotChecked = right.filter(c => !c.checked);
setLeft(left.concat(rightChecked));
setRight(rightNotChecked);
};
const handleAllLeft = () => {
setLeft(left.concat(right));
setRight([]);
};
const customList = (title, items) => (
<>
<CardHeader className={classes.cardHeader} title={title} />
<StyledDivider classes={{root: classes.divider}}/>
<CardContent style={{flexGrow: 1, overflowY: "auto", padding: 0}}>
<List dense component="div" role="list" style={{padding:0, width: "100%"}}>
{items.map((valueObj) => {
const value = valueObj.cmd + " (" + valueObj?.payloadtype?.name + ")";
const labelId = `transfer-list-item-${value}-label`;
return (
<ListItem style={{padding:0}} key={value} role="listitem" button onClick={handleToggle(valueObj)}>
<ListItemIcon>
<Checkbox
checked={checked.findIndex( (element) => element.cmd + " (" + element?.payloadtype?.name + ")" === value) !== -1}
tabIndex={-1}
disableRipple
inputProps={{ 'aria-labelledby': labelId }}
/>
</ListItemIcon>
<ListItemText id={labelId} primary={value} />
</ListItem>
);
})}
<ListItem />
</List>
</CardContent>
</>
);
const setFinalTags = () => {
// things to add are in the `right` now but weren't for `originalRight`
const commandsToAdd = right.filter( (command) => {
@@ -172,14 +185,14 @@ 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}}>
<div style={{paddingLeft: 0, flexGrow: 1, marginLeft: 0, marginRight: "10px", position: "relative", overflowY: "auto", display: "flex", flexDirection: "column" }}>
{customList(leftTitle, left)}
</div>
<div style={{paddingLeft: 0, flexGrow: 1, marginLeft: 0, marginRight: "10px", position: "relative", overflowY: "auto", display: "flex", flexDirection: "column" }}>
<CustomList title={leftTitle} items={left} handleToggle={handleToggle}/>
</div>
<div style={{display: "flex", flexDirection: "column", justifyContent: "center"}}>
<StyledButton
variant="contained"
@@ -196,7 +209,6 @@ export function AddRemoveCallbackCommandsDialog(props) {
size="small"
className={classes.button}
onClick={handleCheckedRight}
disabled={leftChecked.length === 0}
aria-label="move selected right"
>
&gt;
@@ -206,7 +218,6 @@ export function AddRemoveCallbackCommandsDialog(props) {
size="small"
className={classes.button}
onClick={handleCheckedLeft}
disabled={rightChecked.length === 0}
aria-label="move selected left"
>
&lt;
@@ -223,8 +234,8 @@ export function AddRemoveCallbackCommandsDialog(props) {
</StyledButton>
</div>
<div style={{marginLeft: "10px", position: "relative", flexGrow: 1, display: "flex", flexDirection: "column" }}>
{customList(rightTitle, right)}
<div style={{marginLeft: "10px", position: "relative", flexGrow: 1, display: "flex", flexDirection: "column" }}>
<CustomList title={rightTitle} items={right} handleToggle={handleToggle} />
</div>
</div>
</DialogContent>
@@ -10,13 +10,13 @@ import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl';
import Select from '@mui/material/Select';
import { Typography } from '@mui/material';
import ReactFlow, {
applyEdgeChanges, applyNodeChanges,
import { ReactFlow,
EdgeLabelRenderer,getBezierPath, BaseEdge,
Handle, Position, useReactFlow, ReactFlowProvider, Panel,
MiniMap, Controls, ControlButton, useUpdateNodeInternals,
getConnectedEdges
} from 'reactflow';
import 'reactflow/dist/style.css';
getConnectedEdges, useNodesState, useEdgesState
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { toPng, toSvg } from 'html-to-image';
import InsertPhotoIcon from '@mui/icons-material/InsertPhoto';
import CameraAltIcon from '@mui/icons-material/CameraAlt';
@@ -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;
@@ -383,20 +384,23 @@ function AgentNode({data}) {
return {[offsetComponents.location]: (size * index) + (size / 2)}
}
const additionalStyles = data?.selected ? {
boxShadow: `3px 3px 5px 0px ${theme.palette.secondary.main}, inset 0px 0px 60px 0px ${theme.palette.info.main}`,
borderRadius: "20px"
const additionalStyles = data?.anySelected ? data?.selected ? {
boxShadow: `0px 0px 5px 0px ${theme.palette.secondary.main}`,
borderRadius: "5px"
} : {
filter: "grayscale(1)",
opacity: 0.5
} : {};
return (
<div style={{padding: 0, margin: 0, ...additionalStyles}}>
{
[...Array(data.sourceCount)].map((e, i) => (
<Handle type={"source"} id={`${i+1}`} key={`${i+1}`} style={data.sourceCount > 1 ? getOffset(i) : {}} position={sourcePosition} />
<Handle type={"source"} id={`${i+1}`} key={`${i+1}`} style={data.sourceCount > 1 ? getOffset(i) : {}} isConnectable={false} position={sourcePosition} />
))
}
<img alt={data.img} style={{margin: "auto"}} src={data.img} className={"circleImageNode"} />
<Handle type={"target"} position={targetPosition} />
<Handle type={"target"} position={targetPosition} isConnectable={false}/>
<Typography style={{textAlign: "center", margin: 0, padding: 0}} >{data.label}</Typography>
</div>
)
@@ -406,13 +410,13 @@ function TaskNode({data}) {
const targetPosition = getTargetPosition(data["elk.direction"]);
return (
<div >
<Handle type={"source"} position={sourcePosition} />
<Handle type={"source"} position={sourcePosition} isConnectable={false} />
{data.id > 0 &&
<TaskLabelFlat task={data} showOnSelectTask={!data.selected} onSelectTask={() => {data.onSelectTask(data)}}
graphView={true}
/>
}
<Handle type={"target"} position={targetPosition} />
<Handle type={"target"} position={targetPosition} isConnectable={false} />
</div>
)
}
@@ -420,18 +424,21 @@ function BrowserscriptNode({data}) {
const theme = useTheme();
const sourcePosition = getSourcePosition(data["elk.direction"]);
const targetPosition = getTargetPosition(data["elk.direction"]);
const additionalStyles = data?.selected ? {
boxShadow: `5px 5px 25px 5px ${theme.palette.info.main}, inset 0px 0px 60px 0px ${theme.palette.info.main}`,
borderRadius: "20px"
const additionalStyles = data?.anySelected ? data?.selected ? {
boxShadow: `0px 0px 5px 0px ${theme.palette.secondary.main}`,
borderRadius: "5px"
} : {
filter: "grayscale(1)",
opacity: 0.5
} : {};
return (
<div style={{padding: 0, margin: 0, display: "flex", flexDirection: "column", ...additionalStyles}}>
<Handle type={"source"} position={sourcePosition} />
<Handle type={"source"} position={sourcePosition} isConnectable={false} />
{data.img}
<div style={{top: 0, right: 0, height: "50%", width: "50%", position: "absolute"}}>
{data.overlay_img}
</div>
<Handle type={"target"} position={targetPosition} />
<Handle type={"target"} position={targetPosition} isConnectable={false} />
<Typography style={{textAlign: "center", margin: 0, padding: 0}} >{data.data.label}</Typography>
</div>
)
@@ -441,11 +448,11 @@ export function GroupNode({data}) {
const targetPosition = getTargetPosition(data["elk.direction"]);
return (
<>
<Handle hidden type={"source"} position={sourcePosition} />
<Handle hidden type={"source"} position={sourcePosition} isConnectable={false} />
<div className={"groupNode"} style={{width: data.width, height: data.height, margin: "auto"}}>
<Typography style={{textAlign: "center", margin: 0, padding: 0}} >{data.label}</Typography>
</div>
<Handle type={"target"} hidden position={targetPosition} />
<Handle type={"target"} hidden position={targetPosition} isConnectable={false} />
</>
@@ -453,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"){
@@ -601,7 +643,7 @@ export default async function createLayout({initialGroups, initialNodes, initial
position: { x: child.x, y: child.y },
data: { label: child.id, ...child.data, width: child.width, height: child.height, ...options },
style: { width: child.width, height: child.height },
parentNode: current.id
parentId: current.id
})
);
@@ -686,26 +728,19 @@ export const DrawC2PathElementsFlowWithProvider = (props) => {
</ReactFlowProvider>
)
}
export const DrawC2PathElementsFlow = ({edges, panel, view_config, theme, contextMenu, providedNodes}) =>{
export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu, providedNodes, filterOptions}) =>{
const theme = useTheme();
const [graphData, setGraphData] = React.useState({nodes: [], edges: [], groups: []});
const [nodes, setNodes] = React.useState();
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const selectedNodes = React.useRef([]);
const extraNodes = React.useRef(providedNodes);
const [edgeFlow, setEdgeFlow] = React.useState([]);
const [edgeFlow, setEdgeFlow, onEdgesChange] = useEdgesState([]);
const [openContextMenu, setOpenContextMenu] = React.useState(false);
const [contextMenuCoord, setContextMenuCord] = React.useState({});
const viewportRef = React.useRef(null);
const contextMenuNode = React.useRef(null);
const {fitView} = useReactFlow()
const updateNodeInternals = useUpdateNodeInternals();
const onNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[]
);
const onEdgesChange = useCallback(
(changes) => setEdgeFlow((eds) => applyEdgeChanges(changes, eds)),
[]
);
const onDownloadImageClickSvg = () => {
// we calculate a transform for the nodes so that all nodes are visible
// we then overwrite the transform of the `.react-flow__viewport` element
@@ -770,7 +805,7 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, theme, contex
});
setEdgeFlow(updatedEdges);
const updatedNodes = nodes.map( n => {
return {...n, data: {...n.data, selected: false}};
return {...n, data: {...n.data, selected: false, anySelected: false}};
});
setNodes(updatedNodes);
//setGraphData({...graphData, edges: updatedEdges});
@@ -815,9 +850,9 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, theme, contex
const updatedNodes = nodes.map( n => {
let isSelected = selectedNodes.current.filter( s => s.id === n.id).length > 0;
if(isSelected){
return {...n, data: {...n.data, selected: true}};
return {...n, data: {...n.data, selected: true, anySelected: selectedNodes.current.length > 0}};
} else {
return {...n, data: {...n.data, selected: false}};
return {...n, data: {...n.data, selected: false, anySelected: selectedNodes.current.length > 0}};
}
});
setNodes(updatedNodes);
@@ -832,7 +867,7 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, theme, contex
data: {label: "Mythic", img: "/static/mythic.svg", isMythic: true}
}];
let tempEdges = [];
let parentNodes = [];
let parentIds = [];
const add_edge_to_mythic = (edge, view_config) => {
if(!edge.source.active && !view_config["show_all_nodes"]){return}
@@ -855,11 +890,14 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, theme, contex
mythic_id: edge.id,
source: `${edge.source.id}`,
target: "Mythic",
label: edge.c2profile.name,
type: "C2IconEdge",
animated: true,
label: edge.c2profile.name,
data: {
source: {...edge.source, parentNode: getGroupBy(edge.source, view_config)},
target: {parentNode: "Mythic"},
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,
}
},
@@ -891,9 +929,9 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, theme, contex
type: "agentNode",
height: 50,
width: 50,
parentNode: shouldUseGroups(view_config) ? groupByValue : null,
group: shouldUseGroups(view_config) ? groupByValue : null,
extent: shouldUseGroups(view_config) ? "parent" : null,
parentId: shouldUseGroups(view_config) ? groupByValue : undefined,
group: shouldUseGroups(view_config) ? groupByValue : undefined,
extent: shouldUseGroups(view_config) ? "parent" : undefined,
data: {
label: getLabel(node, view_config["label_components"]),
img: "/static/" + node.payload.payloadtype.name + "_" + theme.palette.mode + ".svg",
@@ -905,14 +943,14 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, theme, contex
)
}
found = false;
for(let i = 0; i < parentNodes.length; i++){
if(parentNodes[i].id === groupByValue){
for(let i = 0; i < parentIds.length; i++){
if(parentIds[i].id === groupByValue){
found = true;
break;
}
}
if(!found){
parentNodes.push({
parentIds.push({
id: groupByValue,
position: { x: 110, y: 110 },
type: "groupNode",
@@ -971,10 +1009,13 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, theme, contex
target: `${edge.source.id}`,
label: edge.c2profile.name,
animated: true,
type: "C2IconEdge",
data: {
end_timestamp: edge.end_timestamp,
source: {...edge.destination, parentNode: getGroupBy(edge.destination, view_config)},
target: {...edge.source, parentNode: getGroupBy(edge.source, view_config)},
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)},
}
},
)
@@ -1005,10 +1046,13 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, theme, contex
target: `${edge.destination.id}`,
label: edge.c2profile.name,
animated: true,
type: "C2IconEdge",
data: {
end_timestamp: edge.end_timestamp,
source: {...edge.source, parentNode: getGroupBy(edge.source, view_config)},
target: {...edge.target, parentNode: getGroupBy(edge.target, view_config)},
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)},
}
},
)
@@ -1041,10 +1085,13 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, theme, contex
target: `${edge.destination.id}`,
label: edge.c2profile.name,
animated: true,
type: "C2IconEdge",
data: {
end_timestamp: edge.end_timestamp,
source: {...edge.source, parentNode: getGroupBy(edge.source, view_config)},
target: {...edge.target, parentNode: getGroupBy(edge.destination, view_config)},
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)},
}
},
)
@@ -1063,7 +1110,7 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, theme, contex
}
const hasFakeEdge = (sourceID) => {
for(let i = 0; i < tempEdges.length; i++){
if(tempEdges[i].data.source.parentNode === sourceID &&
if(tempEdges[i].data.source.parentId === sourceID &&
tempEdges[i].data.label === ""
){
return true;
@@ -1081,12 +1128,33 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, theme, contex
}
return false;
}
const filterRow = (row) => {
if(filterOptions === undefined){return false}
for(const [key,value] of Object.entries(filterOptions)){
if(key === "agent"){
if(!String(row.payload.payloadtype.name).toLowerCase().includes(String(value).toLowerCase())){
return true;
}
}else{
if(!String(row[key]).toLowerCase().includes(String(value).toLowerCase())){
return true;
}
}
}
return false;
}
const createNewEdges = () => {
// loop through until all edges have one side marked as "toward_mythic"
let edgesToUpdate = edges.length;
let tempNewEdges = edges.reduce((prev, cur) => {
if(filterRow(cur.source) || filterRow(cur.destination)){
return [...prev];
}
return [...prev, cur];
}, []);
let edgesToUpdate = tempNewEdges.length;
if (edgesToUpdate === 0) {return []}
let edgesUpdated = 0;
let tempNewEdges = [...edges];
let toMythicIds = new Set();
let loop_count = 0;
while(edgesUpdated < edgesToUpdate){
@@ -1144,6 +1212,9 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, theme, contex
if(extraNodes.current){
for(let i = 0; i < extraNodes.current.length; i++){
if(view_config["include_disconnected"]) {
if(filterRow(extraNodes.current[i])){
continue
}
add_node(extraNodes.current[i], view_config);
}
}
@@ -1198,25 +1269,25 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, theme, contex
for(let i = 0; i < tempEdges.length; i++){
if(tempEdges[i].data.end_timestamp === null){
tempEdges[i].markerEnd = {
color: theme.palette.info.main,
color: theme.palette.success.main,
}
tempEdges[i].style = {
stroke: theme.palette.info.main,
stroke: theme.palette.success.main,
strokeWidth: 2,
}
} else {
tempEdges[i].markerEnd = {
color: theme.palette.warning.main,
color: theme.palette.error.main,
}
tempEdges[i].style = {
stroke: theme.palette.warning.main,
stroke: theme.palette.error.main,
strokeWidth: 2,
}
}
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,
@@ -1227,47 +1298,49 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, theme, contex
}
if(shouldUseGroups(view_config)){
// only add in edges from parents to parents/mythic if we're doing egress flow
for(let i = 0; i < parentNodes.length; i++){
for(let i = 0; i < parentIds.length; i++){
// every parentNode needs a connection to _something_ - either to Mythic or another parentNode
for(let j = 0; j < tempEdges.length; j++){
//console.log("checking", parentNodes[i].id, tempEdges[j].data.source.parentNode, tempEdges[j].data.target.parentNode)
if(tempEdges[j].data.source.parentNode === parentNodes[i].id){
if(tempEdges[j].data.source.parentId === parentIds[i].id){
// don't process where source.parentNode == target.parentNode
if(parentNodes[i].id === tempEdges[j].data.target.parentNode){
if(parentIds[i].id === tempEdges[j].data.target.parentId){
//console.log("skipping")
continue
}
if(!hasFakeEdge(`${parentNodes[i].id}`)){
if(!hasFakeEdge(`${parentIds[i].id}`)){
//console.log("adding new fake edge")
tempEdges.push(
{
id: `e${parentNodes[i].id}-${tempEdges[j].data.target.parentNode}`,
id: `e${parentIds[i].id}-${tempEdges[j].data.target.parentId}`,
mythic_id: 0,
source: `${parentNodes[i].id}`,
target: tempEdges[j].data.target.parentNode,
source: `${parentIds[i].id}`,
target: tempEdges[j].data.target.parentId,
label: "",
hidden: true,
type: "C2IconEdge",
data: {
source: {parentNode: parentNodes[i].id},
source: {parentId: parentIds[i].id},
target: tempEdges[j].data.target,
label: "",
end_timestamp: null
}
},
)
} else if(tempEdges[j].data.target.parentNode === "Mythic") {
} else if(tempEdges[j].data.target.parentId === "Mythic") {
//console.log("fake edge is for Mythic, and one exists already - update it")
for(let k = tempEdges.length-1; k >= 0 ; k--){
if(tempEdges[k].source === parentNodes[i].id){
if(tempEdges[k].source === parentIds[i].id){
tempEdges[k] = {
id: `e${parentNodes[i].id}-${tempEdges[j].data.target.parentNode}`,
id: `e${parentIds[i].id}-${tempEdges[j].data.target.parentId}`,
mythic_id: 0,
source: `${parentNodes[i].id}`,
target: tempEdges[j].data.target.parentNode,
source: `${parentIds[i].id}`,
target: tempEdges[j].data.target.parentId,
label: "",
hidden: true,
type: "C2IconEdge",
data: {
source: {parentNode: parentNodes[i].id},
source: {parentId: parentIds[i].id},
target: tempEdges[j].data.target,
label: "",
end_timestamp: null
@@ -1283,15 +1356,15 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, theme, contex
}
tempNodes.push({
id: `${parentNodes[i].id}-widthAdjuster`,
id: `${parentIds[i].id}-widthAdjuster`,
position: { x: 0, y: 0 },
type: "agentNode",
height: 100,
width: 50,
parentNode: `${parentNodes[i].id}`,
group: `${parentNodes[i].id}`,
parentId: `${parentIds[i].id}`,
group: `${parentIds[i].id}`,
hidden: true,
data: {label: `${parentNodes[i].id}`}
data: {label: `${parentIds[i].id}`}
})
}
}
@@ -1305,13 +1378,13 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, theme, contex
}
tempNodes[i].data.sourceCount = sourceCount;
}
//console.log([...parentNodes, ...tempNodes], tempEdges);
//console.log([...tempNodes], tempEdges);
setGraphData({
groups: shouldUseGroups(view_config) ? parentNodes : [],
groups: shouldUseGroups(view_config) ? parentIds : [],
nodes: tempNodes,
edges: tempEdges
})
}, [edges, view_config, theme]);
}, [edges, view_config, theme, filterOptions]);
React.useEffect( () => {
(async () => {
if(graphData.nodes.length > 0){
@@ -1345,6 +1418,7 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, theme, contex
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onPaneClick={onPaneClick}
onNodeContextMenu={onNodeContextMenu}
onNodeClick={onNodeSelected}
@@ -1375,13 +1449,6 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, theme, contex
)
}
export const DrawTaskElementsFlowWithProvider = (props) => {
return (
<ReactFlowProvider>
<DrawTaskElementsFlow {...props} />
</ReactFlowProvider>
)
}
const shouldUseTaskGroups = (view_config) => {
if(view_config.group_by !== "None"){
return true;
@@ -1401,284 +1468,6 @@ const getGroupTaskBy = (node, view_config) => {
}
}
}
export const DrawTaskElementsFlow = ({edges, panel, view_config, theme, contextMenu}) =>{
const [graphData, setGraphData] = React.useState({nodes: [], edges: [], groups: [], view_config});
const [nodes, setNodes] = React.useState();
const [edgeFlow, setEdgeFlow] = React.useState([]);
const [openContextMenu, setOpenContextMenu] = React.useState(false);
const [contextMenuCoord, setContextMenuCord] = React.useState({});
const viewportRef = React.useRef(null);
const contextMenuNode = React.useRef(null);
const [localViewConfig, setLocalViewConfig] = React.useState(view_config);
const {fitView} = useReactFlow();
const updateNodeInternals = useUpdateNodeInternals();
const onNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[]
);
const onEdgesChange = useCallback(
(changes) => setEdgeFlow((eds) => applyEdgeChanges(changes, eds)),
[]
);
const onNodeContextMenu = useCallback( (event, node) => {
if(!contextMenu){return}
if(node.type === "groupNode"){
return;
}
event.preventDefault();
contextMenuNode.current = {...node.data, id: node.data.callback_id};
setContextMenuCord({
top: event.clientY,
left: event.clientX,
});
setOpenContextMenu(true);
}, [contextMenu])
const onPaneClick = useCallback( () => {
setOpenContextMenu(false);
}, [setOpenContextMenu])
React.useEffect( () => {
let tempNodes = [];
let tempEdges = [];
let parentNodes = [];
const add_node = (node, localViewConfig) => {
let groupByValue = getGroupTaskBy(node, localViewConfig);
let nodeID = `${node.id}`;
let found = false;
for(let i = 0; i < tempNodes.length; i++){
if(tempNodes[i].id === nodeID){
found = true;
break;
}
}
if(!found){
//console.log("adding node", node)
tempNodes.push(
{
id: `${node.id}`,
position: { x: 0, y: 0 },
type: "taskNode",
height: 50,
width: 100,
parentNode: shouldUseTaskGroups(localViewConfig) && groupByValue !== "" ? groupByValue : null,
group: shouldUseTaskGroups(localViewConfig) && groupByValue !== "" ? groupByValue : null,
extent: shouldUseTaskGroups(localViewConfig) && groupByValue !== "" ? "parent" : null,
data: {
...node,
parentNode: shouldUseTaskGroups(localViewConfig) && groupByValue !== "" ? groupByValue : null,
label: "",
}
}
)
}
found = false;
for(let i = 0; i < parentNodes.length; i++){
if(parentNodes[i].id === groupByValue){
found = true;
break;
}
}
if(!found && groupByValue !== ""){
//console.log("adding parent", node)
parentNodes.push({
id: groupByValue,
position: { x: 110, y: 110 },
type: "groupNode",
width: 200,
height: 200,
data: {
label: groupByValue,
},
});
}
}
const add_edge_p2p = (edge, localViewConfig) => {
add_node(edge.source, localViewConfig);
add_node(edge.destination, localViewConfig);
if(edge.source.id === edge.destination.id){
return
}
createEdge(edge, localViewConfig);
}
const createEdge = (edge, localViewConfig) =>{
let edgeID = `e${edge.source.id}-${edge.destination.id}`;
//console.log("adding edge", edge);
let groupByValueSource = getGroupTaskBy(edge.source, localViewConfig);
let groupByValueDestination = getGroupTaskBy(edge.destination, localViewConfig);
tempEdges.push(
{
id: edgeID,
source: `${edge.source.id}`,
target: `${edge.destination.id}`,
animated: true,
label: "subtask",
data: {
label: "subtask",
source: {...edge.source, parentNode: shouldUseTaskGroups(localViewConfig) && groupByValueSource !== "" ? groupByValueSource : null},
target: {...edge.destination, parentNode: shouldUseTaskGroups(localViewConfig) && groupByValueDestination !== "" ? groupByValueDestination : null},
}
},
)
}
const hasFakeEdge = (sourceID) => {
for(let i = 0; i < tempEdges.length; i++){
if(tempEdges[i].data.source.parentNode === sourceID &&
tempEdges[i].data.label === ""
){
return true;
}
}
return false;
}
edges.forEach( (edge) => {
add_edge_p2p(edge, localViewConfig);
});
for(let i = 0; i < tempEdges.length; i++){
tempEdges[i].markerEnd = {
color: theme.palette.info.main,
}
tempEdges[i].style = {
stroke: theme.palette.info.main,
strokeWidth: 2,
}
tempEdges[i].markerEnd.type = "arrowclosed"
tempEdges[i].labelBgStyle = {
fill: theme.tableHover,
fillOpacity: 0.6,
}
tempEdges[i].labelStyle = {
fill: theme.palette.background.contrast,
}
tempEdges[i].labelShowBg = true
tempEdges[i].zIndex = 20;
}
if(shouldUseTaskGroups(localViewConfig)){
// only add in edges from parents to parents/mythic if we're doing egress flow
for(let i = 0; i < parentNodes.length; i++){
// every parentNode needs a connection to _something_ - either to Mythic or another parentNode
for(let j = 0; j < tempEdges.length; j++){
//console.log("checking", parentNodes[i].id, tempEdges[j].data.target.parentNode, tempEdges[j].data.source.id)
if(tempEdges[j].data.target.parentNode === parentNodes[i].id){
// don't process where source.parentNode == target.parentNode
//console.log("found match")
if(parentNodes[i].id === tempEdges[j].data.source.parentNode){
//console.log("skipping")
continue
}
if(!hasFakeEdge(`${parentNodes[i].id}`)){
//console.log("adding new fake edge")
tempEdges.push(
{
id: `e${parentNodes[i].id}-${tempEdges[j].data.source.id}`,
target: `${parentNodes[i].id}`,
source: `${tempEdges[j].data.source.id}`,
label: "",
hidden: true,
data: {
source: {...parentNodes[i], parentNode: `${parentNodes[i].id}`},
target: tempEdges[j].data.target,
label: "",
}
},
)
}
}
}
tempNodes.push({
id: `${parentNodes[i].id}-widthAdjuster`,
position: { x: 0, y: 0 },
type: "agentNode",
height: 100,
width: 50,
parentNode: `${parentNodes[i].id}`,
group: `${parentNodes[i].id}`,
hidden: true,
data: {label: `${parentNodes[i].id}`}
})
}
}
//console.log("parent groups", shouldUseTaskGroups(view_config), [...parentNodes, ...tempNodes], tempEdges);
setGraphData({
groups: shouldUseTaskGroups(localViewConfig) ? parentNodes : [],
nodes: tempNodes,
edges: tempEdges,
view_config: {...localViewConfig},
});
}, [edges, localViewConfig, theme]);
React.useEffect( () => {
(async () => {
if(graphData.nodes.length > 0){
const {newNodes, newEdges} = await createLayout({
initialGroups: graphData.groups,
initialNodes: graphData.nodes,
initialEdges: graphData.edges,
alignment: graphData.view_config.rankDir
});
setNodes(newNodes);
setEdgeFlow(newEdges);
for(let i = 0; i < newNodes.length; i++){
updateNodeInternals(newNodes[i].id);
}
//console.log("new graph data", newNodes, newEdges)
window.requestAnimationFrame(() => {
for(let i = 0; i < newNodes.length; i++){
updateNodeInternals(newNodes[i].id);
}
fitView();
});
}
})();
}, [graphData]);
const toggleViewConfig = () => {
if(localViewConfig.rankDir === "LR"){
setLocalViewConfig({...localViewConfig, rankDir: "BT", group_by: "name"});
} else {
setLocalViewConfig({...localViewConfig, rankDir: "LR", group_by: "name"});
}
}
return (
<div style={{height: "100%", width: "100%"}} ref={viewportRef}>
<ReactFlow
fitView
onlyRenderVisibleElements={false}
panOnScrollSpeed={50}
maxZoom={100}
minZoom={0}
nodes={nodes}
edges={edgeFlow}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodeTypes={nodeTypes}
onPaneClick={onPaneClick}
onNodeContextMenu={onNodeContextMenu}
>
<Panel position={"top-left"} >{panel}</Panel>
<Controls showInteractive={false} >
<ControlButton onClick={toggleViewConfig} title={"Toggle View"}>
<SwapCallsIcon />
</ControlButton>
</Controls>
</ReactFlow>
{openContextMenu &&
<div style={{...contextMenuCoord, position: "fixed"}} className="context-menu">
{contextMenu.map( (m) => (
<Button key={m.title} color={"info"} className="context-menu-button" onClick={() => {
m.onClick(contextMenuNode.current);
setOpenContextMenu(false);
}}>{m.title}</Button>
))}
</div>
}
</div>
)
}
const getGroupBrowserscriptBy = (node, view_config) => {
if(view_config.group_by === undefined){return ""}
if(view_config.group_by === "None" || view_config.group_by === ""){return ""}
@@ -1695,7 +1484,7 @@ export const DrawBrowserScriptElementsFlowWithProvider = (props) => {
</ReactFlowProvider>
)
}
const DrawBrowserScriptElementsFlow = ({edges, panel, view_config, theme, contextMenu, providedNodes, task}) => {
export const DrawBrowserScriptElementsFlow = ({edges, panel, view_config, theme, contextMenu, providedNodes, task}) => {
const [graphData, setGraphData] = React.useState({nodes: [], edges: [], groups: [], view_config});
const selectedNodes = React.useRef([]);
const selectedEdges = React.useRef([]);
@@ -1712,8 +1501,8 @@ const DrawBrowserScriptElementsFlow = ({edges, panel, view_config, theme, contex
setOpenTableButton(false);
setTaskingData({});
}
const [nodes, setNodes] = React.useState([]);
const [edgeFlow, setEdgeFlow] = React.useState([]);
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edgeFlow, setEdgeFlow, onEdgesChange] = useEdgesState([]);
const [openContextMenu, setOpenContextMenu] = React.useState(false);
const [contextMenuCoord, setContextMenuCord] = React.useState({});
const viewportRef = React.useRef(null);
@@ -1721,16 +1510,6 @@ const DrawBrowserScriptElementsFlow = ({edges, panel, view_config, theme, contex
const [localViewConfig, setLocalViewConfig] = React.useState(view_config);
const {fitView} = useReactFlow();
const updateNodeInternals = useUpdateNodeInternals();
const onNodesChange = useCallback(
(changes) => {
setNodes((nds) => applyNodeChanges(changes, nds));
},
[]
);
const onEdgesChange = useCallback(
(changes) => setEdgeFlow((eds) => applyEdgeChanges(changes, eds)),
[]
);
const onPaneContextMenu = useCallback( (event) => {
event.preventDefault();
contextMenuNode.current = {};
@@ -1756,7 +1535,7 @@ const DrawBrowserScriptElementsFlow = ({edges, panel, view_config, theme, contex
});
const updatedNodes = nodes.map( n => {
return {...n, data: {...n.data, selected: false}};
return {...n, data: {...n.data, selected: false, anySelected: false}};
});
setNodes(updatedNodes);
setEdgeFlow(updatedEdges);
@@ -1987,9 +1766,9 @@ const DrawBrowserScriptElementsFlow = ({edges, panel, view_config, theme, contex
const updatedNodes = nodes.map( n => {
let isSelected = selectedNodes.current.filter( s => s.id === n.id).length > 0;
if(isSelected){
return {...n, data: {...n.data, selected: true}};
return {...n, data: {...n.data, selected: true, anySelected: selectedNodes.current.length > 0}};
} else {
return {...n, data: {...n.data, selected: false}};
return {...n, data: {...n.data, selected: false, anySelected: selectedNodes.current.length > 0}};
}
});
//setGraphData({...graphData, edges: updatedEdges});
@@ -2059,7 +1838,7 @@ const DrawBrowserScriptElementsFlow = ({edges, panel, view_config, theme, contex
React.useEffect( () => {
let tempNodes = [];
let tempEdges = [];
let parentNodes = [];
let parentIds = [];
const add_node = (node, localViewConfig) => {
@@ -2081,27 +1860,27 @@ const DrawBrowserScriptElementsFlow = ({edges, panel, view_config, theme, contex
type: "browserscriptNode",
height: 50,
width: 50,
parentNode: shouldUseTaskGroups(localViewConfig) && groupByValue !== "" ? groupByValue : null,
group: shouldUseTaskGroups(localViewConfig) && groupByValue !== "" ? groupByValue : null,
extent: shouldUseTaskGroups(localViewConfig) && groupByValue !== "" ? "parent" : null,
parentId: shouldUseTaskGroups(localViewConfig) && groupByValue !== "" ? groupByValue : undefined,
group: shouldUseTaskGroups(localViewConfig) && groupByValue !== "" ? groupByValue : undefined,
extent: shouldUseTaskGroups(localViewConfig) && groupByValue !== "" ? "parent" : undefined,
data: {
...node,
parentNode: shouldUseTaskGroups(localViewConfig) && groupByValue !== "" ? groupByValue : null,
parentId: shouldUseTaskGroups(localViewConfig) && groupByValue !== "" ? groupByValue : undefined,
label: "",
}
}
)
}
found = false;
for(let i = 0; i < parentNodes.length; i++){
if(parentNodes[i].id === groupByValue){
for(let i = 0; i < parentIds.length; i++){
if(parentIds[i].id === groupByValue){
found = true;
break;
}
}
if(!found && groupByValue !== ""){
//console.log("adding parent", node)
parentNodes.push({
parentIds.push({
id: groupByValue,
position: { x: 110, y: 110 },
type: "groupNode",
@@ -2140,15 +1919,15 @@ const DrawBrowserScriptElementsFlow = ({edges, panel, view_config, theme, contex
data: {
...edge,
label: `${edge.label}`,
source: {...edge.source, parentNode: shouldUseTaskGroups(localViewConfig) && groupByValueSource !== "" ? groupByValueSource : null},
target: {...edge.destination, parentNode: shouldUseTaskGroups(localViewConfig) && groupByValueDestination !== "" ? groupByValueDestination : null},
source: {...edge.source, parentId: shouldUseTaskGroups(localViewConfig) && groupByValueSource !== "" ? groupByValueSource : undefined},
target: {...edge.destination, parentId: shouldUseTaskGroups(localViewConfig) && groupByValueDestination !== "" ? groupByValueDestination : undefined},
}
},
)
}
const hasFakeEdge = (sourceID) => {
for(let i = 0; i < tempEdges.length; i++){
if(tempEdges[i].data.source.parentNode === sourceID &&
if(tempEdges[i].data.source.parentId === sourceID &&
tempEdges[i].data.label === ""
){
return true;
@@ -2211,28 +1990,28 @@ const DrawBrowserScriptElementsFlow = ({edges, panel, view_config, theme, contex
}
if(shouldUseTaskGroups(localViewConfig)){
// only add in edges from parents to parents/mythic if we're doing egress flow
for(let i = 0; i < parentNodes.length; i++){
for(let i = 0; i < parentIds.length; i++){
// every parentNode needs a connection to _something_ - either to Mythic or another parentNode
for(let j = 0; j < tempEdges.length; j++){
//console.log("checking", parentNodes[i].id, tempEdges[j].data.target.parentNode, tempEdges[j].data.source.id)
if(tempEdges[j].data.target.parentNode === parentNodes[i].id){
if(tempEdges[j].data.target.parentId === parentIds[i].id){
// don't process where source.parentNode == target.parentNode
//console.log("found match")
if(parentNodes[i].id === tempEdges[j].data.source.parentNode){
if(parentIds[i].id === tempEdges[j].data.source.parentId){
//console.log("skipping")
continue
}
if(!hasFakeEdge(`${parentNodes[i].id}`)){
if(!hasFakeEdge(`${parentIds[i].id}`)){
//console.log("adding new fake edge")
tempEdges.push(
{
id: `e${parentNodes[i].id}-${tempEdges[j].data.source.id}`,
target: `${parentNodes[i].id}`,
id: `e${parentIds[i].id}-${tempEdges[j].data.source.id}`,
target: `${parentIds[i].id}`,
source: `${tempEdges[j].data.source.id}`,
label: "",
hidden: true,
data: {
source: {...parentNodes[i], parentNode: `${parentNodes[i].id}`},
source: {...parentIds[i], parentId: `${parentIds[i].id}`},
target: tempEdges[j].data.target,
label: "",
}
@@ -2243,22 +2022,22 @@ const DrawBrowserScriptElementsFlow = ({edges, panel, view_config, theme, contex
}
tempNodes.push({
id: `${parentNodes[i].id}-widthAdjuster`,
id: `${parentIds[i].id}-widthAdjuster`,
position: { x: 0, y: 0 },
type: "browserscriptNode",
height: 100,
width: 50,
parentNode: `${parentNodes[i].id}`,
group: `${parentNodes[i].id}`,
parentId: `${parentIds[i].id}`,
group: `${parentIds[i].id}`,
hidden: true,
data: {label: `${parentNodes[i].id}`}
data: {label: `${parentIds[i].id}`}
})
}
}
//console.log("parent groups", shouldUseTaskGroups(view_config), [...parentNodes, ...tempNodes], tempEdges);
setGraphData({
groups: shouldUseTaskGroups(localViewConfig) ? parentNodes : [],
groups: shouldUseTaskGroups(localViewConfig) ? parentIds : [],
nodes: tempNodes,
edges: tempEdges,
view_config: {...localViewConfig},
@@ -16,6 +16,14 @@ mutation hideCallback ($callback_display_id: Int!){
}
}
`;
export const updateCallbackTriggerMutation = gql`
mutation updateCallbackTriggerMutation ($callback_display_id: Int!, $trigger_on_checkin_after_time: Int!){
updateCallback(input: {callback_display_id: $callback_display_id, trigger_on_checkin_after_time: $trigger_on_checkin_after_time}) {
status
error
}
}
`;
export const toggleHideCallbackMutations = gql`
mutation hideCallback ($callback_display_id: Int!, $active: Boolean!){
updateCallback(input: {callback_display_id: $callback_display_id, active: $active}) {
@@ -59,8 +67,8 @@ mutation lockCallack($callback_display_id: Int!){
}
`;
export const updateDescriptionCallbackMutation = gql`
mutation updateDescriptionCallack($callback_display_id: Int!, $description: String!){
updateCallback(input: {callback_display_id: $callback_display_id, description: $description}) {
mutation updateDescriptionCallback($callback_display_id: Int!, $description: String!, $color: String!){
updateCallback(input: {callback_display_id: $callback_display_id, description: $description, color: $color}) {
status
error
}
@@ -87,6 +95,8 @@ export const taskingDataFragment = gql`
fragment taskData on task {
comment
parent_task_id
agent_task_id
tasking_location
callback {
display_id
id
@@ -96,6 +106,7 @@ export const taskingDataFragment = gql`
domain
ip
mythictree_groups
pid
}
callback_id
commentOperator{
@@ -123,6 +134,8 @@ export const taskingDataFragment = gql`
status
timestamp
status_timestamp_submitted
status_timestamp_processing
status_timestamp_preprocessing
command {
cmd
supported_ui_features
@@ -134,14 +147,23 @@ 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
tasks(where: {is_interactive_task: {_eq: false}}, order_by: {id: asc}) {
id
comment
agent_task_id
commentOperator{
username
}
@@ -154,6 +176,7 @@ export const taskingDataFragment = gql`
domain
ip
mythictree_groups
pid
}
completed
subtask_group_name
@@ -1,9 +1,5 @@
import React, { useEffect } from 'react';
import { styled } from '@mui/material/styles';
import { CallbacksTabs } from './CallbacksTabs';
import SpeedDial from '@mui/material/SpeedDial';
import SpeedDialIcon from '@mui/material/SpeedDialIcon';
import SpeedDialAction from '@mui/material/SpeedDialAction';
import TocIcon from '@mui/icons-material/Toc';
import AssessmentIcon from '@mui/icons-material/Assessment';
import { CallbacksTop } from './CallbacksTop';
@@ -11,46 +7,11 @@ import Split from 'react-split';
import PhoneForwardedIcon from '@mui/icons-material/PhoneForwarded';
import {MythicDialog} from "../../MythicComponents/MythicDialog";
import {ImportCallbackConfigDialog} from "./ImportCallbackConfigDialog";
import {reorder} from "../../MythicComponents/MythicDraggableList";
import Paper from '@mui/material/Paper';
import IconButton from '@mui/material/IconButton';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
const PREFIX = 'Callbacks';
const classes = {
root: `${PREFIX}-root`,
speedDial: `${PREFIX}-speedDial`,
speedDialAction: `${PREFIX}-speedDialAction`,
tooltip: `${PREFIX}-tooltip`,
arrow: `${PREFIX}-arrow`
};
const StyledSpeedDial = styled(SpeedDial)(({theme}) => ({
[`&.${classes.speedDial}`]: {
position: 'absolute',
'&.MuiSpeedDial-directionUp, &.MuiSpeedDial-directionLeft': {
bottom: theme.spacing(2),
right: theme.spacing(2),
},
'&.MuiSpeedDial-directionDown, &.MuiSpeedDial-directionRight': {
top: theme.spacing(2),
right: theme.spacing(2),
},
zIndex: 4,
},
[`& .${classes.speedDialAction}`]: {
backgroundColor: theme.palette.speedDialAction,
},
[`& .${classes.tooltip}`]: {
backgroundColor: theme.palette.background.contrast,
color: theme.palette.text.contrast,
boxShadow: theme.shadows[1],
fontSize: 13,
},
[`& .${classes.arrow}`]: {
color: theme.palette.background.contrast,
}
}));
export const getCallbackIdFromClickedTab = (tabId) => {
if(tabId === null || tabId === undefined){return 0}
if(tabId === ""){return 0}
@@ -58,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;
@@ -67,7 +32,9 @@ 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([]);
@@ -84,6 +51,7 @@ export function Callbacks({me}) {
}
}
setClickedTabIdValue(tabID);
setNewDataForTab((prev) => {return {...prev, [tabID]: false}});
}
useEffect(() => {
const oldTabs = localStorage.getItem('openTabs');
@@ -113,6 +81,12 @@ export function Callbacks({me}) {
}, [openTabs])
const onOpenTab = React.useRef( (tabData) => {
let found = false;
openTabRef.current = openTabRef.current.map( (tab) => {
if(tab.tabID === tabData.tabID){
return {...tabData};
}
return {...tab};
})
openTabRef.current.forEach((tab) => {
if (tab.tabID === tabData.tabID) found = true;
});
@@ -121,6 +95,8 @@ export function Callbacks({me}) {
const tabs = [...openTabRef.current, { ...tabData }];
localStorage.setItem('openTabs', JSON.stringify(tabs));
setOpenTabs(tabs);
} else {
setOpenTabs([...openTabRef.current]);
}
localStorage.setItem('clickedTab', tabData.tabID);
setClickedTabId(tabData.tabID);
@@ -192,7 +168,7 @@ export function Callbacks({me}) {
newOpenTabList.push(openTabs[targetTabIndex]);
newOpenTabList.push(openTabs[i]);
}
setClickedTabId(openTabs[targetTabIndex].tabID)
} else {
newOpenTabList.push(openTabs[i]);
@@ -225,15 +201,49 @@ export function Callbacks({me}) {
}
},
];
const onDragEnd = ({ destination, source }) => {
// dropped outside the list
if (!destination) return;
const newItems = reorder(openTabs, source.index, destination.index);
setOpenTabs(newItems);
localStorage.setItem('openTabs', JSON.stringify(newItems));
};
return (
<>
<SpeedDialWrapper setTopDisplay={setTopDisplay} />
<Split direction="vertical"
sizes={callbackTableSplitSizes}
minSize={[0,0]}
onDragEnd={(sizes) => localStorage.setItem('callbackTableSplitSizes', JSON.stringify(sizes))}
style={{ height: "100%" }}>
<div className="bg-gray-base">
<div style={{display: "flex", flexDirection: "row-reverse"}}>
<Paper elevation={5} style={{width: "30px", display: "flex", flexDirection: "column", alignItems: "center", overflow: "hidden",
backgroundColor: "transparent"}}>
{topDisplay !== 'table' &&
<MythicStyledTooltip title={"Table View"}>
<IconButton onClick={() =>setTopDisplay("table")}>
<TocIcon />
</IconButton>
</MythicStyledTooltip>
}
{topDisplay !== 'graph' &&
<MythicStyledTooltip title={"Graph View"} >
<IconButton onClick={() =>setTopDisplay("graph")}>
<AssessmentIcon />
</IconButton>
</MythicStyledTooltip>
}
{openCallbackImport &&
<MythicDialog fullWidth={true} maxWidth="sm" open={openCallbackImport}
onClose={()=>{setOpenCallbackImport(false);}}
innerDialog={<ImportCallbackConfigDialog onClose={()=>{setOpenCallbackImport(false);}} />}
/>
}
<MythicStyledTooltip title={"Import previously exported Callbacks"} >
<IconButton onClick={() =>setOpenCallbackImport(true)}>
<PhoneForwardedIcon />
</IconButton>
</MythicStyledTooltip>
</Paper>
<CallbacksTop
callbackTableGridRef={callbackTableGridRef}
topDisplay={topDisplay}
@@ -241,7 +251,7 @@ export function Callbacks({me}) {
onOpenTabs={onOpenTabs.current}
me={me} clickedTabId={clickedTabId}/>
</div>
<div className="bg-gray-mid">
<div >
<CallbacksTabs
onCloseTab={onCloseTab}
onEditTabDescription={onEditTabDescription}
@@ -251,108 +261,13 @@ export function Callbacks({me}) {
openTabs={openTabs}
onDragTab={onDragTab}
me={me}
onDragEnd={onDragEnd}
contextMenuOptions={contextMenuOptions}
newDataForTab={newDataForTab}
setNewDataForTab={setNewDataForTab}
/>
</div>
</Split>
</>
);
}
/*
<div style={{ maxWidth: '100%', height: '100%', flexDirection: 'column'}}>
<React.Fragment>
<SpeedDialWrapper setTopDisplay={setTopDisplay} heights={heights} onSubmitHeights={onSubmitHeights} />
<div style={{flexGrow: 1, flexBasis: heights.top, height: heights.top }}>
<CallbacksTop topDisplay={topDisplay} onOpenTab={onOpenTab.current} heights={heights} me={me}/>
</div>
<div style={{ flexGrow: 1, flexBasis: heights.bottom, height: heights.bottom }}>
<CallbacksTabs
onCloseTab={onCloseTab}
onEditTabDescription={onEditTabDescription}
tabHeight={heights.bottom}
maxHeight={heights.bottom}
key={'callbackstabs'}
clickedTabId={clickedTabId}
openTabs={openTabs}
onDragTab={onDragTab}
me={me}
contextMenuOptions={contextMenuOptions}
/>
</div>
</React.Fragment>
</div>
*/
function SpeedDialWrapperPreMemo({ setTopDisplay }) {
const [open, setOpen] = React.useState(false);
const [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:"15px"}}
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,4 +1,4 @@
import React, {useRef, useEffect, useState, useMemo, useContext} from 'react';
import React, {useEffect, useState, useMemo, useContext} from 'react';
import {DrawC2PathElementsFlowWithProvider} from './C2PathDialog';
import {Button} from '@mui/material';
import ButtonGroup from '@mui/material/ButtonGroup';
@@ -21,6 +21,8 @@ import FormControl from '@mui/material/FormControl';
import Select from '@mui/material/Select';
import {CallbackGraphEdgesContext, CallbacksContext, OnOpenTabContext} from './CallbacksTop';
import {Dropdown, DropdownMenuItem} from "../../MythicComponents/MythicNestedMenus";
import {GetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
import {operatorSettingDefaults} from "../../../cache";
const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 1;
@@ -134,7 +136,7 @@ const GraphViewOptions = ({viewConfig, setViewConfig}) => {
</Button>
</ButtonGroup>
{showConfiguration &&
<FormControl sx={{ width: 200, marginTop: "10px", backgroundColor: theme.palette.background.default}} >
<FormControl sx={{ width: 200, backgroundColor: theme.palette.background.paper}} >
<InputLabel id="demo-chip-label">Group Callbacks By</InputLabel>
<Select
labelId="demo-chip-label"
@@ -155,7 +157,7 @@ const GraphViewOptions = ({viewConfig, setViewConfig}) => {
</FormControl>
}
{showConfiguration &&
<FormControl sx={{minWidth: 300, marginTop: "10px",backgroundColor: theme.palette.background.default}}>
<FormControl sx={{minWidth: 300, backgroundColor: theme.palette.background.paper}}>
<InputLabel id="demo-multiple-chip-label">Display Properties per Callback</InputLabel>
<Select
labelId="demo-multiple-chip-label"
@@ -201,10 +203,11 @@ const GraphViewOptions = ({viewConfig, setViewConfig}) => {
)
}
export function CallbacksGraph({onOpenTab}){
const theme = useTheme();
const callbacks = useContext(CallbacksContext);
const callbackgraphedges = useContext(CallbackGraphEdgesContext);
//used for creating a task to do a link command
const [loadingSettings, setLoadingSettings] = React.useState(true);
const [filterOptions, setFilterOptions] = React.useState({});
const [linkCommands, setLinkCommands] = React.useState([]);
const [openParametersDialog, setOpenParametersDialog] = React.useState(false);
const [openSelectLinkCommandDialog, setOpenSelectLinkCommandDialog] = React.useState(false);
@@ -363,14 +366,40 @@ export function CallbacksGraph({onOpenTab}){
}
},
]}, [getLinkCommands, hideCallback, viewConfig, onOpenTab]);
React.useEffect( () => {
try {
const storageItemOptions = GetMythicSetting({setting_name: "callbacks_table_filter_options", default_value: operatorSettingDefaults.callbacks_table_filters});
if(storageItemOptions !== null){
setFilterOptions(storageItemOptions);
}
}catch(error){
console.log("Failed to load callbacks_table_filter_options", error);
}
setLoadingSettings(false);
}, []);
if(loadingSettings){
return (<div style={{width: '100%', height: '100%', position: "relative",}}>
<div style={{overflowY: "hidden", flexGrow: 1}}>
<div style={{
position: "absolute",
left: "35%",
top: "40%"
}}>
{"Loading Callbacks and Connections..."}
</div>
</div>
</div>)
}
return (
<div style={{maxWidth: "100%", "overflow": "hidden", height: "100%"}}>
{manuallyRemoveEdgeDialogOpen &&
<MythicDialog fullWidth={true} maxWidth="sm" open={manuallyRemoveEdgeDialogOpen}
onClose={()=>{setManuallyRemoveEdgeDialogOpen(false);}}
innerDialog={<MythicSelectFromListDialog onClose={()=>{setManuallyRemoveEdgeDialogOpen(false);}} identifier="id" display="display"
onClose={() => {
setManuallyRemoveEdgeDialogOpen(false);
}}
innerDialog={<MythicSelectFromListDialog onClose={() => {
setManuallyRemoveEdgeDialogOpen(false);}} identifier="id" display="display"
onSubmit={onSubmitManuallyRemoveEdge} options={edgeOptions} title={"Manually Remove Edge"} action={"remove"} />}
/>
}
@@ -395,8 +424,9 @@ export function CallbacksGraph({onOpenTab}){
action={"select"} display={"display"} identifier={"display"}/>}
/>
}
<DrawC2PathElementsFlowWithProvider providedNodes={callbacks} edges={callbackgraphedges} view_config={viewConfig} theme={theme}
panel={<GraphViewOptions setViewConfig={setViewConfig} viewConfig={viewConfig} />} contextMenu={contextMenu}/>
<DrawC2PathElementsFlowWithProvider providedNodes={callbacks} edges={callbackgraphedges} view_config={viewConfig}
filterOptions={filterOptions}
panel={<GraphViewOptions setViewConfig={setViewConfig} viewConfig={viewConfig} />} contextMenu={contextMenu}/>
</div>
);
}
@@ -1,44 +1,32 @@
import React, { useEffect, useMemo, useContext} from 'react';
import {MythicTransferListDialog} from '../../MythicComponents/MythicTransferList';
import {MythicDialog, MythicModifyStringDialog} from '../../MythicComponents/MythicDialog';
import {
exportCallbackConfigQuery,
hideCallbackMutation, lockCallbackMutation, unlockCallbackMutation,
hideCallbackMutation, lockCallbackMutation, unlockCallbackMutation, updateCallbackTriggerMutation,
updateDescriptionCallbackMutation,
updateSleepInfoCallbackMutation
} from './CallbackMutations';
import {snackActions} from '../../utilities/Snackbar';
import {useMutation, useLazyQuery } from '@apollo/client';
import {useMutation, useLazyQuery, gql } from '@apollo/client';
import {
CallbacksTableIDCell,
CallbacksTableStringCell,
CallbacksTableLastCheckinCell,
CallbacksTablePayloadTypeCell,
CallbacksTableC2Cell,
CallbacksTableOSCell,
CallbacksTableSleepCell,
CallbacksTableIPCell
CallbacksTableIDCell,
CallbacksTableStringCell,
CallbacksTableLastCheckinCell,
CallbacksTablePayloadTypeCell,
CallbacksTableC2Cell,
CallbacksTableOSCell,
CallbacksTableSleepCell,
CallbacksTableIPCell, CallbacksTableTagsCell
} from './CallbacksTableRow';
import MythicResizableGrid from '../../MythicComponents/MythicResizableGrid';
import {TableFilterDialog} from './TableFilterDialog';
import {CallbacksTabsHideMultipleDialog} from "./CallbacksTabsHideMultipleDialog";
import {CallbacksTabsHideMultipleDialog, CallbacksTabsSelectMultipleDialog} from "./CallbacksTabsHideMultipleDialog";
import {CallbacksTabsTaskMultipleDialog} from "./CallbacksTabsTaskMultipleDialog";
import ip6 from 'ip6';
import {CallbacksContext, OnOpenTabContext, OnOpenTabsContext} from "./CallbacksTop";
import {
MaterialReactTable,
useMaterialReactTable,
} from 'material-react-table';
import {useTheme} from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import {GetMythicSetting, SetMythicSetting, useMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
import {GetMythicSetting, useSetMythicSetting, GetComputedFontSize} from "../../MythicComponents/MythicSavedUserSetting";
import {DetailedCallbackTable} from "./DetailedCallbackTable";
import {ModifyCallbackMythicTreeGroupsDialog} from "./ModifyCallbackMythicTreeGroupsDialog";
import Paper from '@mui/material/Paper';
import Grow from '@mui/material/Grow';
import Popper from '@mui/material/Popper';
import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import {getCallbackIdFromClickedTab} from './Callbacks';
import {Dropdown, DropdownNestedMenuItem, DropdownMenuItem} from "../../MythicComponents/MythicNestedMenus";
@@ -58,7 +46,35 @@ import LockIcon from '@mui/icons-material/Lock';
import AccountTreeIcon from '@mui/icons-material/AccountTree';
import {TaskFromUIButton} from "./TaskFromUIButton";
import {CallbacksTabsOpenMultipleDialog} from "./CallbacksTabsOpenMultipleDialog";
import {operatorSettingDefaults} from "../../../cache";
import {CallbacksTableEditDescriptionColorDialog} from "./CallbacksTableEditDescriptionColorDialog";
import NotificationsActiveTwoToneIcon from '@mui/icons-material/NotificationsActiveTwoTone';
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}
@@ -97,13 +113,34 @@ export const ipCompare = (a, b) => {
return 0;
}
}
const callbackTableInitialColumns = [
{key: "id", type: 'number', name: "Interact", width: 150, disableDoubleClick: true},
{key: "mythictree_groups", type: 'array', name: "Groups", width: 150},
{key: "ip", type: 'ip', name: "IP", width: 150},
{key: "external_ip",type: 'string', name: "External IP", width: 150},
{key: "host", type: 'string', name: "Host", fillWidth: true},
{key: "user", type: 'string', name: "User", fillWidth: true},
{key: "domain", type: 'string', name: "Domain", fillWidth: true},
{key: "os", type: 'string', name: "OS", width: 45, disableDoubleClick: true, disableSort: true},
{key: "architecture", type: 'string', name: "Arch", width: 75},
{key: "pid", type: 'number', name: "PID", width: 75},
{key: "last_checkin", type: 'timestamp', name: "Last Checkin", width: 150, disableFilterMenu: true, disableDoubleClick: true},
{key: "description", type: 'string', name: "Description", width: 400},
{key: "sleep", type: 'string', name: "Sleep", width: 60, disableFilterMenu: true, disableSort: true, disableDoubleClick: true},
{key: "agent", type: 'agent', name: "Agent", width: 150},
{key: "c2", type: 'string', name: "C2", width: 45, disableSort: true, disableFilterMenu: true, disableDoubleClick: true},
{key: "process_short_name", type: 'string', name: "Process Name", fillWidth: true},
{key: "tags", type: 'tags', name: "Tags", fillWidth: false, disableDoubleClick: true, width: 150}
];
function CallbacksTablePreMemo(props){
const callbacks = useContext(CallbacksContext);
const onOpenTab = useContext(OnOpenTabContext);
const onOpenTabs = useContext(OnOpenTabsContext);
const interactType = GetMythicSetting({setting_name: "interactType", default_value: "interact", output: "string"});
const theme = useTheme();
const [loadingSettings, setLoadingSettings] = React.useState(true);
const [columnOrder, setColumnOrder] = React.useState(callbackTableInitialColumns);
const [openReorderDialog, setOpenReorderDialog] = React.useState(false);
const [openMultipleTabsDialog, setOpenMultipleTabsDialog] = React.useState({open: false, tabType: "interact"});
const [openMetaDialog, setOpenMetaDialog] = React.useState(false);
const openMetaDialogRef = React.useRef(0);
@@ -121,7 +158,7 @@ function CallbacksTablePreMemo(props){
const [updateDescriptionMutation] = useMutation(updateDescriptionCallbackMutation, {
update: (cache, {data}) => {
if(data.updateCallback.status === "success"){
snackActions.success("Updated Callback");
//snackActions.success("Updated Callback");
}else{
snackActions.warning(data.updateCallback.error);
}
@@ -132,31 +169,34 @@ function CallbacksTablePreMemo(props){
snackActions.warning(data);
}
});
const updateDescriptionRef = React.useRef({payload_description: "", callback_display_id: 0, description: ""});
const updateDescription = ({payload_description, callback_display_id, description}) => {
const updateDescriptionRef = React.useRef({payload_description: "", callback_display_id: 0, description: "", color: ""});
const updateDescription = ({payload_description, callback_display_id, description, color}) => {
updateDescriptionRef.current = {
payload_description: payload_description,
callback_display_id: callback_display_id,
description: description
description: description,
color: color,
};
setOpenEditDescriptionDialog(true);
}
const editDescriptionSubmit = (description) => {
const editDescriptionSubmit = (description, color) => {
setOpenEditDescriptionDialog(false);
if(description === ""){
updateDescriptionSubmit({
description: updateDescriptionRef.current.payload_description,
color: color,
callback_display_id: updateDescriptionRef.current.callback_display_id
});
} else {
updateDescriptionSubmit({
description: description,
color: color,
callback_display_id: updateDescriptionRef.current.callback_display_id
});
}
}
const updateDescriptionSubmit = React.useCallback( ({callback_display_id, description}) => {
updateDescriptionMutation({variables: {callback_display_id: callback_display_id, description}})
const updateDescriptionSubmit = React.useCallback( ({callback_display_id, description, color}) => {
updateDescriptionMutation({variables: {callback_display_id: callback_display_id, description, color}})
}, []);
const [openCallbackDropdown, setOpenCallbackDropdown] = React.useState(false);
const callbackDropdownRef = React.useRef({options: [], callback: {}});
@@ -177,16 +217,17 @@ function CallbacksTablePreMemo(props){
};
const [sortData, setSortData] = React.useState({"sortKey": null, "sortDirection": null, "sortType": null});
const [openContextMenu, setOpenContextMenu] = React.useState(false);
const [openAdjustColumnsDialog, setOpenAdjustColumnsDialog] = React.useState(false);
const [openHideMultipleDialog, setOpenHideMultipleDialog] = React.useState(false);
const [openTriggerDialog, setOpenTriggerDialog] = React.useState({open: false, trigger_on_checkin_after_time: 0, display_id: 0});
const [openTaskMultipleDialog, setOpenTaskMultipleDialog] = React.useState({open: false, data: {}});
const [filterOptions, setFilterOptions] = React.useState({});
const [selectedColumn, setSelectedColumn] = React.useState({});
const [columnVisibility, setColumnVisibility] = React.useState(() => {
let defaults = {"visible": ["Interact", "Host", "Domain", "User", "Description", "Last Checkin", "Agent", "IP", "PID"],
"hidden": ["Arch", "Sleep", "Process Name", "External IP", "C2", "OS", "Groups"]}
"hidden": ["Arch", "Sleep", "Process Name", "External IP", "C2", "OS", "Groups", "Tags"]}
try {
const storageItem = GetMythicSetting({setting_name: "callbacks_table_columns", default_value: {}, output: "json"});
const storageItem = GetMythicSetting({setting_name: "callbacks_table_columns", default_value: operatorSettingDefaults.callbacks_table_columns});
if(storageItem !== null){
let allColumns = [...defaults["visible"].map(c => c), ...defaults["hidden"].map(c => c)];
let newHidden = [];
@@ -215,7 +256,7 @@ function CallbacksTablePreMemo(props){
const [hideCallback] = useMutation(hideCallbackMutation, {
update: (cache, {data}) => {
if(data.updateCallback.status === "success"){
snackActions.success("Hiding callback");
//snackActions.success("Hiding callback");
}else{
snackActions.warning(data.updateCallback.error);
}
@@ -253,6 +294,20 @@ function CallbacksTablePreMemo(props){
snackActions.warning(data);
}
});
const [updateTrigger] = useMutation(updateCallbackTriggerMutation, {
update: (cache, {data}) => {
if(data.updateCallback.status === "success"){
snackActions.success("Updated Trigger Threshold");
}else{
snackActions.warning(data.updateCallback.error);
}
},
onError: data => {
console.log(data);
snackActions.warning(data);
}
})
const [exportConfig] = useLazyQuery(exportCallbackConfigQuery, {
fetchPolicy: "no-cache",
onCompleted: (data) => {
@@ -261,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();
}
@@ -279,10 +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 onRowContextClick = ({rowDataStatic}) => {
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});
}
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"){
@@ -290,10 +357,39 @@ 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, icon: null, click: ({event}) => {},
type: "item", disabled: true
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
},
{
name: "Interact", icon: defaultInteractIcon, click: ({event}) => {
@@ -303,15 +399,17 @@ function CallbacksTablePreMemo(props){
tabType: tabType,
tabID: rowDataStatic.id + tabType,
callbackID: rowDataStatic.id,
color: rowDataStatic.color,
displayID: rowDataStatic.display_id});
}, type: "item"
},
{
name: "Edit Description", icon: <EditIcon style={{paddingRight: "5px"}} />, click:({event}) => {
name: "Edit Description and Color", icon: <EditIcon style={{paddingRight: "5px"}} />, click:({event}) => {
event.stopPropagation();
updateDescription({payload_description: rowDataStatic.payload.description,
callback_display_id: rowDataStatic.display_id,
description: rowDataStatic.description,
color: rowDataStatic.color
});
}, type: "item"
},
@@ -322,7 +420,7 @@ function CallbacksTablePreMemo(props){
}, type: "item"
},
{
name: "Exit Callback", icon: <FontAwesomeIcon icon={faSkullCrossbones} style={{color: theme.errorOnMain, cursor: "pointer", marginRight: "10px"}} />,
name: "Exit Callback", icon: <FontAwesomeIcon icon={faSkullCrossbones} style={{color: theme.palette.error.main, cursor: "pointer", marginRight: "8px"}} />,
click: ({event}) => {
taskingData.current = {
"parameters": "",
@@ -336,7 +434,8 @@ function CallbacksTablePreMemo(props){
}, type: "item"
},
{
name: rowDataStatic.locked ? 'Unlock (Locked by ' + rowDataStatic.locked_operator.username + ')' : 'Lock Callback', icon: rowDataStatic.locked ? (<LockIcon color={"error"} style={{paddingRight: "5px"}}/>) : (<LockOpenIcon color={"success"} style={{paddingRight: "5px"}} />),
name: rowDataStatic.locked ? 'Unlock (Locked by ' + rowDataStatic.locked_operator.username + ')' : 'Lock Callback',
icon: rowDataStatic.locked ? (<LockIcon color={"error"} style={{paddingRight: "5px"}}/>) : (<LockOpenIcon color={"success"} style={{paddingRight: "5px"}} />),
click: ({event}) => {
event.stopPropagation();
if(rowDataStatic.locked){
@@ -346,33 +445,72 @@ function CallbacksTablePreMemo(props){
}
}, type: "item"
},
{
name: rowDataStatic.trigger_on_checkin_after_time > 0 ? "Adjust Alert Trigger" : "Add New Alert Trigger",
type: "item",
icon: rowDataStatic.trigger_on_checkin_after_time > 0 ? (<NotificationsOffTwoToneIcon color={"warning"} style={{paddingRight: "5px"}} />) : (<NotificationsActiveTwoToneIcon color={"success"} style={{paddingRight: "5px"}}/>),
click: ({event}) => {
event.stopPropagation();
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({
tabType: tabType,
tabID: rowDataStatic.id + tabType,
callbackID: rowDataStatic.id,
color: rowDataStatic.color,
displayID: rowDataStatic.display_id});
}
},
{
name: 'Process Browser', icon: <AccountTreeIcon style={{paddingRight: "5px"}}/>,
click: ({event}) => {
event.preventDefault();
event.stopPropagation();
const tabType = "processBrowser";
onOpenTab({
tabType: tabType,
tabID: rowDataStatic.id + tabType,
callbackID: rowDataStatic.id,
color: rowDataStatic.color,
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
]
},
{
@@ -390,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);
}
}
]
},
{
@@ -403,6 +547,7 @@ function CallbacksTablePreMemo(props){
tabType: tabType,
tabID: rowDataStatic.id + tabType,
callbackID: rowDataStatic.id,
color: rowDataStatic.color,
displayID: rowDataStatic.display_id});
}
},
@@ -414,6 +559,7 @@ function CallbacksTablePreMemo(props){
tabType: tabType,
tabID: rowDataStatic.id + tabType,
callbackID: rowDataStatic.id,
color: rowDataStatic.color,
displayID: rowDataStatic.display_id});
}
},
@@ -425,6 +571,7 @@ function CallbacksTablePreMemo(props){
tabType: tabType,
tabID: rowDataStatic.id + tabType,
callbackID: rowDataStatic.id,
color: rowDataStatic.color,
displayID: rowDataStatic.display_id});
}
},
@@ -491,20 +638,16 @@ 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);
}
const onSubmitAdjustColumns = ({left, right}) => {
setColumnVisibility({visible: right, hidden: left});
SetMythicSetting({setting_name: "callbacks_table_columns", value: right, output: "json"});
}
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: "callbacks_table_columns", default_value: {}, output: "json"});
const storageItem = GetMythicSetting({setting_name: "callbacks_table_columns", default_value: operatorSettingDefaults.callbacks_table_columns});
if(storageItem !== null){
let allColumns = [...columnVisibility["visible"].map(c => c), ...columnVisibility["hidden"].map(c => c)];
let newHidden = [];
@@ -519,34 +662,37 @@ function CallbacksTablePreMemo(props){
console.log("Failed to load callbacks_table_columns", error);
}
try {
const storageItemOptions = GetMythicSetting({setting_name: "callbacks_table_filter_options", default_value: {}, output: "json"});
const storageItemOptions = GetMythicSetting({setting_name: "callbacks_table_filter_options", default_value: operatorSettingDefaults.callbacks_table_filters});
if(storageItemOptions !== null){
setFilterOptions(storageItemOptions);
}
}catch(error){
console.log("Failed to load callbacks_table_filter_options", error);
}
}
try {
const storageColumnOrder = GetMythicSetting({setting_name: "callbacks_table_column_order", default_value: callbackTableInitialColumns.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 === callbackTableInitialColumns.length){
setColumnOrder(newOrder);
}
}
}catch(error){
console.log("Failed to load callbacks_table_filter_options", error);
}
setLoadingSettings(false);
}, [])
const columns = useMemo(
() =>
[
{key: "id", type: 'number', name: "Interact", width: 120, disableDoubleClick: true},
{key: "mythictree_groups", type: 'array', name: "Groups"},
{key: "ip", type: 'ip', name: "IP", width: 150},
{key: "external_ip",type: 'string', name: "External IP", width: 150},
{key: "host", type: 'string', name: "Host", fillWidth: true},
{key: "user", type: 'string', name: "User", fillWidth: true},
{key: "domain", type: 'string', name: "Domain", fillWidth: true},
{key: "os", type: 'string', name: "OS", width: 45, disableDoubleClick: true, disableSort: true},
{key: "architecture", type: 'string', name: "Arch", width: 75},
{key: "pid", type: 'number', name: "PID", width: 75},
{key: "last_checkin", type: 'timestamp', name: "Last Checkin", width: 150, disableFilterMenu: true, disableDoubleClick: true},
{key: "description", type: 'string', name: "Description", width: 400},
{key: "sleep", type: 'string', name: "Sleep", width: 60, disableFilterMenu: true, disableSort: true, disableDoubleClick: true},
{key: "agent", type: 'agent', name: "Agent", width: 150},
{key: "c2", type: 'string', name: "C2", width: 45, disableSort: true, disableFilterMenu: true, disableDoubleClick: true},
{key: "process_short_name", type: 'string', name: "Process Name", fillWidth: true},
].reduce( (prev, cur) => {
() =>
columnOrder.reduce( (prev, cur) => {
if(columnVisibility.visible.includes(cur.name) || cur.name === "Interact"){
if(filterOptions[cur.key] && String(filterOptions[cur.key]).length > 0){
return [...prev, {...cur, filtered: true}];
@@ -557,7 +703,7 @@ function CallbacksTablePreMemo(props){
return [...prev];
}
}, [])
, [filterOptions, columnVisibility]
, [filterOptions, columnVisibility, columnOrder]
);
const onClickHeader = (e, columnIndex) => {
const column = columns[columnIndex];
@@ -565,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) => {
@@ -583,32 +729,34 @@ function CallbacksTablePreMemo(props){
tabType: tabType,
tabID: rowDataStatic.id + tabType,
callbackID: rowDataStatic.id,
color: rowDataStatic.color,
displayID: rowDataStatic.display_id});
}, []);
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: "Show/Hide Columns",
name: "Reorder Columns and Adjust Visibility", type: "item", icon: null,
click: ({event, columnIndex}) => {
event.preventDefault();
event.stopPropagation();
if(columns[columnIndex].disableFilterMenu){
snackActions.warning("Can't filter that column");
return;
if(event){
event.stopPropagation();
event.preventDefault();
}
setOpenAdjustColumnsDialog(true);
setOpenReorderDialog(true);
}
}
];
@@ -673,7 +821,10 @@ function CallbacksTablePreMemo(props){
switch(c.name){
case "Interact":
return <CallbacksTableIDCell
rowData={{...row, selected: row.id === clickedCallbackID}}
rowData={{...row,
selected: row.id === clickedCallbackID,
rowStyle: {backgroundColor: `${row.color}`},
}}
key={`callback${row.id}_${c.name}`}
callbackDropdown={callbackDropdown}
/>;
@@ -681,68 +832,120 @@ function CallbacksTablePreMemo(props){
return <CallbacksTableStringCell
key={`callback${row.id}_${c.name}`}
cellData={row.mythictree_groups.join(", ")}
rowData={{...row, selected: row.id === clickedCallbackID}}
rowData={{...row,
selected: row.id === clickedCallbackID,
rowStyle: {backgroundColor: `${row.color}`},
}}
/>;
case "IP":
return <CallbacksTableIPCell
key={`callback${row.id}_${c.name}`}
cellData={row.ip}
rowData={{...row, selected: row.id === clickedCallbackID}}
rowData={{...row,
selected: row.id === clickedCallbackID,
rowStyle: {backgroundColor: `${row.color}`},
}}
callback_id={row.id} />;
case "External IP":
return <CallbacksTableStringCell key={`callback${row.id}_${c.name}`} cellData={row.external_ip}
rowData={{...row, selected: row.id === clickedCallbackID}} />;
rowData={{...row,
selected: row.id === clickedCallbackID,
rowStyle: {backgroundColor: `${row.color}`},
}} />;
case "Host":
return <CallbacksTableStringCell key={`callback${row.id}_${c.name}`} cellData={row.host}
rowData={{...row, selected: row.id === clickedCallbackID}} />;
rowData={{...row,
selected: row.id === clickedCallbackID,
rowStyle: {backgroundColor: `${row.color}`},
}} />;
case "User":
return <CallbacksTableStringCell key={`callback${row.id}_${c.name}`} cellData={row.user}
rowData={{...row, selected: row.id === clickedCallbackID}} />;
return <CallbacksTableStringCell key={`callback${row.id}_${c.name}`} cellData={row.user + (row.impersonation_context === "" ? "" : ` [${row.impersonation_context}]`)}
rowData={{...row,
selected: row.id === clickedCallbackID,
rowStyle: {backgroundColor: `${row.color}`},
}} />;
case "Domain":
return <CallbacksTableStringCell key={`callback${row.id}_${c.name}`} cellData={row.domain}
rowData={{...row, selected: row.id === clickedCallbackID}} />;
rowData={{...row,
selected: row.id === clickedCallbackID,
rowStyle: {backgroundColor: `${row.color}`},
}} />;
case "OS":
return <CallbacksTableOSCell key={`callback${row.id}_${c.name}`}
rowData={{...row, selected: row.id === clickedCallbackID}}
rowData={{...row,
selected: row.id === clickedCallbackID,
rowStyle: {backgroundColor: `${row.color}`},
}}
cellData={row.os} />;
case "Arch":
return <CallbacksTableStringCell key={`callback${row.id}_${c.name}`}
rowData={{...row, selected: row.id === clickedCallbackID}}
rowData={{...row,
selected: row.id === clickedCallbackID,
rowStyle: {backgroundColor: `${row.color}`},
}}
cellData={row.architecture} />;
case "PID":
return <CallbacksTableStringCell key={`callback${row.id}_${c.name}`} cellData={row.pid}
rowData={{...row, selected: row.id === clickedCallbackID}} />;
rowData={{...row,
selected: row.id === clickedCallbackID,
rowStyle: {backgroundColor: `${row.color}`},
}} />;
case "Last Checkin":
return <CallbacksTableLastCheckinCell key={`callback${row.id}_${c.name}`}
rowData={{...row, selected: row.id === clickedCallbackID}}
return <CallbacksTableLastCheckinCell key={`callback${row.id}_${c.name}`} me={props.me}
rowData={{...row,
selected: row.id === clickedCallbackID,
rowStyle: {backgroundColor: `${row.color}`},
}}
cellData={row.last_checkin} />;
case "Description":
return <CallbacksTableStringCell key={`callback${row.id}_${c.name}`} cellData={row.description}
rowData={{...row, selected: row.id === clickedCallbackID}} />;
rowData={{...row,
selected: row.id === clickedCallbackID,
rowStyle: {backgroundColor: `${row.color}`},
}} />;
case "Sleep":
return <CallbacksTableSleepCell key={`callback${row.id}_${c.name}`}
rowData={{...row, selected: row.id === clickedCallbackID}}
rowData={{...row,
selected: row.id === clickedCallbackID,
rowStyle: {backgroundColor: `${row.color}`},
}}
cellData={row.sleep_info} updateSleepInfo={updateSleepInfo} />;
case "Agent":
return <CallbacksTablePayloadTypeCell key={`callback${row.id}_${c.name}`}
rowData={{...row, selected: row.id === clickedCallbackID}}
rowData={{...row,
selected: row.id === clickedCallbackID,
rowStyle: {backgroundColor: `${row.color}`},
}}
cellData={row.payload.payloadtype.name}/>;
case "C2":
return <CallbacksTableC2Cell key={`callback${row.id}_c2`}
rowData={{...row, selected: row.id === clickedCallbackID}} />
rowData={{...row,
selected: row.id === clickedCallbackID,
rowStyle: {backgroundColor: `${row.color}`},
}} />
case "Process Name":
return <CallbacksTableStringCell key={`callback${row.id}_${c.name}`}
cellData={row.process_short_name}
rowData={{...row, selected: row.id === clickedCallbackID}} />;
rowData={{...row,
selected: row.id === clickedCallbackID,
rowStyle: {backgroundColor: `${row.color}`},
}} />;
case "Tags":
return <CallbacksTableTagsCell key={`callback${row.id}_${c.name}`}
cellData={row.tags}
rowData={{...row,
selected: row.id === clickedCallbackID,
rowStyle: {backgroundColor: `${row.color}`},
}} />;
}
})];
}
}, [])
}, [callbacks, sortData, filterOptions, columnVisibility, clickedCallbackID]);
const onSubmitFilterOptions = (newFilterOptions) => {
setFilterOptions(newFilterOptions);
const onSubmitFilterOptions = (value) => {
setFilterOptions({...filterOptions, [selectedColumn.key]: value });
try{
SetMythicSetting({setting_name: "callbacks_table_filter_options", value: newFilterOptions, output: "json"});
updateSetting({setting_name: "callbacks_table_filter_options", value: {...filterOptions, [selectedColumn.key]: value }});
}catch(error){
console.log("failed to save filter options");
}
@@ -755,12 +958,13 @@ function CallbacksTablePreMemo(props){
tabType: openMultipleTabsDialog.tabType,
tabID: tabsToOpen[i].id + openMultipleTabsDialog.tabType,
callbackID: tabsToOpen[i].id,
color: tabsToOpen[i].color,
displayID: tabsToOpen[i].display_id});
}
onOpenTabs({tabs: newTabs});
setOpenMultipleTabsDialog({open: false, tabType: "interact"});
}
if(props.loading){
if(props.loading || loadingSettings){
return (
<div style={{width: '100%', height: '100%', position: "relative",}}>
<div style={{overflowY: "hidden", flexGrow: 1}}>
@@ -775,6 +979,32 @@ function CallbacksTablePreMemo(props){
</div>
)
}
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);
}
}
updateSetting({setting_name: "callbacks_table_column_order", value: newOrder.map(c => c.name)});
setColumnOrder(newOrder);
setColumnVisibility({visible: newVisible, hidden: newHidden});
updateSetting({setting_name: "callbacks_table_columns", value: newVisible});
setOpenReorderDialog(false);
}
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
@@ -784,32 +1014,36 @@ function CallbacksTablePreMemo(props){
sortIndicatorIndex={sortColumn}
sortDirection={sortData.sortDirection}
items={sortedData}
rowHeight={20}
rowHeight={GetComputedFontSize() + 7}
onClickHeader={onClickHeader}
onDoubleClickRow={onRowDoubleClick}
contextMenuOptions={contextMenuOptions}
onRowContextMenuClick={onRowContextClick}
onRowContextMenuClick={onRowContextClick}
/>
{openContextMenu &&
<MythicDialog fullWidth={true} maxWidth="xs" open={openContextMenu}
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
onClose={()=>{setOpenContextMenu(false);}}
innerDialog={<TableFilterDialog
selectedColumn={selectedColumn}
filterOptions={filterOptions}
onSubmit={onSubmitFilterOptions}
onClose={()=>{setOpenContextMenu(false);}} />}
innerDialog={<MythicModifyStringDialog
title='Filter Column'
onSubmit={onSubmitFilterOptions}
value={filterOptions[selectedColumn.key]}
onClose={() => {
setOpenContextMenu(false);
}}
/>}
/>
}
{openEditDescriptionDialog &&
<MythicDialog
fullWidth={true}
maxWidth={"lg"}
maxWidth={"md"}
open={openEditDescriptionDialog}
onClose={() => {setOpenEditDescriptionDialog(false);}}
innerDialog={
<MythicModifyStringDialog title={`Edit Callback ${updateDescriptionRef.current.callback_display_id} Description`}
<CallbacksTableEditDescriptionColorDialog title={`Edit Callback ${updateDescriptionRef.current.callback_display_id} Description and Color`}
onClose={() => {setOpenEditDescriptionDialog(false);}}
value={updateDescriptionRef.current.description}
description={updateDescriptionRef.current.description}
color={updateDescriptionRef.current.color}
onSubmit={editDescriptionSubmit}
/>
}
@@ -885,16 +1119,6 @@ function CallbacksTablePreMemo(props){
/>
</ClickAwayListener>
}
{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"}/>}
/>
}
{openHideMultipleDialog &&
<MythicDialog
fullWidth={true}
@@ -906,6 +1130,33 @@ function CallbacksTablePreMemo(props){
}
/>
}
{openTriggerDialog.open &&
<MythicDialog
fullWidth={true}
maxWidth="md"
open={openTriggerDialog.open}
onClose={() => {setOpenTriggerDialog({...openTriggerDialog, open: false});}}
innerDialog={
<CallbacksTableEditTriggerOnCheckinDialog
onSubmit={onUpdateTrigger}
trigger_on_checkin_after_time={openTriggerDialog.trigger_on_checkin_after_time}
onClose={() => {setOpenTriggerDialog({...openTriggerDialog, open: 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}
/>}
/>
}
{openMultipleTabsDialog.open &&
<MythicDialog
fullWidth={true}
@@ -934,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>
)
}
@@ -0,0 +1,118 @@
import React from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import IconButton from '@mui/material/IconButton';
import { useTheme } from '@mui/material/styles';
import {reorder} from "../../MythicComponents/MythicDraggableList";
import {
Draggable,
DragDropContext,
Droppable,
} from "@hello-pangea/dnd";
import { List, ListItem, ListItemText, Paper } from '@mui/material';
import DragHandleIcon from '@mui/icons-material/DragHandle';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import VisibilityIcon from '@mui/icons-material/Visibility';
export function CallbacksTableColumnsReorderDialog({initialItems, onSubmit, onClose, onReset, visible, hidden}) {
const [items, setItems] = React.useState(initialItems);
const theme = useTheme();
const onDragEnd = ({ destination, source }) => {
// dropped outside the list
if (!destination) return;
const newItems = reorder(items, source.index, destination.index);
setItems(newItems);
};
React.useEffect( () => {
const newItems = items.map( c => {
if(visible.includes(c.name)){
return {...c, visible: true};
}
return {...c, visible: false};
})
setItems(newItems);
}, [visible, hidden]);
const onToggleVisibility = (i) => {
const newItems = items.map( (c, index) => {
if(index === i){
return {...c, visible: !c.visible};
}
return {...c};
});
setItems(newItems);
}
const onFinish = () => {
onSubmit(items);
}
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Drag and Drop to Adjust the Order and Toggle Visibility</DialogTitle>
<DialogContent dividers={true} style={{height: "100%", margin: 0, padding: 0, background: theme.palette.background.main}}>
<DraggableList items={items} onToggleVisibility={onToggleVisibility} onDragEnd={onDragEnd} />
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
Close
</Button>
<Button onClick={onReset} variant="contained" color="warning">
Reset
</Button>
<Button onClick={onFinish} variant="contained" color="success">
Submit
</Button>
</DialogActions>
</React.Fragment>
);
}
export const DraggableList = ({ items, onDragEnd, onToggleVisibility }) => {
return (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="callback-table-column-list">
{(provided) => (
<List style={{border: 0, padding: 0}} ref={provided.innerRef} {...provided.droppableProps}>
{items.map((item, index) => (
<DraggableListItem onToggleVisibility={onToggleVisibility} item={item} index={index} key={item.key} />
))}
{provided.placeholder}
</List>
)}
</Droppable>
</DragDropContext>
);
};
export const DraggableListItem = ({ item, index, onToggleVisibility }) => {
const theme = useTheme();
return (
<Draggable draggableId={item.key} index={index}>
{(provided, snapshot) => (
<ListItem
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
component={Paper}
disabled={!item.visible}
sx={snapshot.isDragging ? { background: theme.palette.secondary.main } : {
}}
>
<DragHandleIcon style={{marginRight: "10px"}}/>
<ListItemText primary={item.name} />
<IconButton onClick={() => onToggleVisibility(index)} style={{float: "right", margin: 0, padding: 0}}>
{item.visible ? (
<VisibilityIcon color={"success"} />
) : (
<VisibilityOffIcon color={"error"} />
)
}
</IconButton>
</ListItem>
)}
</Draggable>
);
};
@@ -0,0 +1,86 @@
import React, {useEffect} from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import Box from '@mui/material/Box';
import AceEditor from 'react-ace';
import "ace-builds/src-noconflict/mode-json";
import "ace-builds/src-noconflict/theme-github";
import "ace-builds/src-noconflict/theme-monokai";
import "ace-builds/src-noconflict/ext-searchbox";
import {useTheme} from '@mui/material/styles';
import {HexColorInput, HexColorPicker} from 'react-colorful';
import Typography from '@mui/material/Typography';
export function CallbacksTableEditDescriptionColorDialog(props) {
const [comment, setComment] = React.useState("");
const [color, setColor] = React.useState("");
const theme = useTheme();
const onCommitSubmit = () => {
if(color === "ffffff" || color === "000000"){
props.onSubmit(comment, "");
} else {
props.onSubmit(comment, color);
}
if(props.dontCloseOnSubmit){
return;
}
props.onClose();
}
const onChange = (value) => {
setComment(value);
}
useEffect( () => {
try{
setComment(JSON.stringify(JSON.parse(props.description), null, 2));
}catch(error){
setComment(props.description);
}
setColor(props.color);
}, [props.description, props.color]);
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">{props.title}</DialogTitle>
<DialogContent dividers={true} style={{height: "100%", margin: 0, padding: 0}}>
<AceEditor
mode="json"
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
width="100%"
height="100%"
fontSize={14}
wrapEnabled={props.wrap ? props.wrap : false}
minLines={props.maxRows ? props.maxRows : 3}
maxLines={props.maxRows ? props.maxRows : 3}
value={comment}
focus={true}
onChange={onChange}
setOptions={{
tabSize: 4,
useWorker: false,
showInvisibles: false,
}}
/>
<HexColorPicker color={color} onChange={setColor} style={{width: "100%"}} />
<HexColorInput color={color} onChange={setColor} style={{width: "80%"}} />
<Button onClick={()=>setColor("ffffff")} color={"success"}>
Clear
</Button>
<Box sx={{width: "100%", height: 25, backgroundColor: color}} >
<Typography style={{color: "white", display: "inline-block", marginRight: "10px"}}>DarkMode Text With Color Background</Typography>
<Typography style={{color: "black", display: "inline-block"}}>LightMode Text With Color Background</Typography>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
Close
</Button>
{props.onSubmit &&
<Button onClick={onCommitSubmit} variant="contained" color="success">
Submit
</Button>
}
</DialogActions>
</React.Fragment>
);
}
@@ -0,0 +1,59 @@
import React, {useEffect} from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import Box from '@mui/material/Box';
import AceEditor from 'react-ace';
import "ace-builds/src-noconflict/mode-json";
import "ace-builds/src-noconflict/theme-github";
import "ace-builds/src-noconflict/theme-monokai";
import "ace-builds/src-noconflict/ext-searchbox";
import {useTheme} from '@mui/material/styles';
import {HexColorInput, HexColorPicker} from 'react-colorful';
import Typography from '@mui/material/Typography';
import MythicTextField from "../../MythicComponents/MythicTextField";
export function CallbacksTableEditTriggerOnCheckinDialog(props) {
const [comment, setComment] = React.useState(0);
const onChange = (name, value, error) => {
if(isNaN(parseInt(value))){
setComment(0);
} else {
setComment(parseInt(value));
}
}
useEffect( () => {
setComment(props.trigger_on_checkin_after_time);
}, [props.trigger_on_checkin_after_time]);
const onSubmit = () => {
props.onSubmit(comment);
}
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">{"Adjust this callback's trigger threshold"}</DialogTitle>
<DialogContent dividers={true} style={{height: "100%"}}>
<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>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
Close
</Button>
{props.onSubmit &&
<Button onClick={onSubmit} variant="contained" color="success">
Submit
</Button>
}
</DialogActions>
</React.Fragment>
);
}
@@ -26,14 +26,18 @@ import { areEqual } from 'react-window';
import {CallbackGraphEdgesContext, OnOpenTabContext} from './CallbacksTop';
import Moment from 'react-moment';
import moment from 'moment';
import {useMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
import {GetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
import TerminalIcon from '@mui/icons-material/Terminal';
import VerticalSplitIcon from '@mui/icons-material/VerticalSplit';
import NotificationsActiveTwoToneIcon from '@mui/icons-material/NotificationsActiveTwoTone';
import {faSocks} from '@fortawesome/free-solid-svg-icons';
import {TagsDisplay, ViewEditTags} from "../../MythicComponents/MythicTag";
export const CallbacksTableIDCell = React.memo(({rowData, metaDialog, updateDescription, editMythicTreeGroupsDialog, setOpenHideMultipleDialog, setOpenTaskMultipleDialog, callbackDropdown}) =>{
export const CallbacksTableIDCell = React.memo(({rowData, callbackDropdown}) =>{
const theme = useTheme();
const dropdownAnchorRef = React.useRef(null);
const onOpenTab = useContext(OnOpenTabContext);
const interactType = useMythicSetting({setting_name: "interactType", default_value: "interact", output: "string"})
const interactType = GetMythicSetting({setting_name: "interactType", default_value: "interact"})
const [rowDataStatic, setRowDataStatic] = React.useState(rowData);
React.useEffect( () => {
let update = false;
@@ -55,10 +59,44 @@ export const CallbacksTableIDCell = React.memo(({rowData, metaDialog, updateDesc
if(rowData.id !== rowDataStatic.id){
update = true;
}
if(rowData.color !== rowDataStatic.color){
update = true;
}
if(rowData.trigger_on_checkin_after_time !== rowDataStatic.trigger_on_checkin_after_time){
update = true;
}
if(rowData.callbackports.length !== rowDataStatic.callbackports.length){
update = true;
}
if(update){
setRowDataStatic(rowData);
}
}, [rowData]);
const proxyMessage = useCallback(() => {
let message = [];
for(let i = 0; i < rowDataStatic?.callbackports?.length || 0; i++){
switch(rowDataStatic.callbackports[i].port_type){
case "socks":
if(rowDataStatic.callbackports[i].username === ""){
message.push(`socks: ${rowDataStatic.callbackports[i].local_port}`);
} else {
message.push(`socks: ${rowDataStatic.callbackports[i].local_port}, ${rowDataStatic.callbackports[i].username}:${rowDataStatic.callbackports[i].password}`);
}
break;
case "rpfwd":
if(rowDataStatic.callbackports[i].username === ""){
message.push(`rpfwd: ${rowDataStatic.callbackports[i].local_port}->${rowDataStatic.callbackports[i].remote_ip}:${rowDataStatic.callbackports[i].remote_port}`);
} else {
message.push(`pfwd: ${rowDataStatic.callbackports[i].local_port}->${rowDataStatic.callbackports[i].remote_ip}:${rowDataStatic.callbackports[i].remote_port}, ${rowDataStatic.callbackports[i].username}:${rowDataStatic.callbackports[i].password}`);
}
break;
case "interactive":
message.push(`interactive: ${rowDataStatic.callbackports[i].local_port}`);
break;
}
}
return message.join("\n");
}, [rowDataStatic]);
const handleDropdownToggle = (event) => {
event.stopPropagation();
callbackDropdown({rowDataStatic, event});
@@ -74,23 +112,32 @@ export const CallbacksTableIDCell = React.memo(({rowData, metaDialog, updateDesc
}
return (
<div id={`callbacksTableID${rowDataStatic.id}`}>
<IconButton style={{padding: 0, margin: 0}} color={rowDataStatic.integrity_level > 2 ? "error" : ""}
onClick={(evt) => {evt.stopPropagation();localOnOpenTab()}}
>
{rowDataStatic.locked ? (<LockIcon style={{marginRight: "10px"}} />):(defaultInteractIcon)}
</IconButton>
{rowDataStatic.display_id}
<IconButton
style={{margin: 0, padding: 0}}
color={rowDataStatic.integrity_level > 2 ? "error" : ""}
aria-haspopup="menu"
onClick={handleDropdownToggle}
ref={dropdownAnchorRef}
>
<ArrowDropDownIcon/>
</IconButton>
<div id={`callbacksTableID${rowDataStatic.id}`} style={{display: "inline-flex", alignItems: "flex-start"}}>
<IconButton style={{padding: 0, margin: 0}} color={rowDataStatic.integrity_level > 2 ? "error" : ""}
onClick={(evt) => {evt.stopPropagation();localOnOpenTab()}}
>
{rowDataStatic.locked ? (<LockIcon style={{marginRight: "10px"}} />):(defaultInteractIcon)}
</IconButton>
{rowDataStatic.display_id}
<IconButton
style={{margin: 0, padding: 0}}
color={rowDataStatic.integrity_level > 2 ? "error" : ""}
aria-haspopup="menu"
onClick={handleDropdownToggle}
ref={dropdownAnchorRef}
>
<ArrowDropDownIcon/>
</IconButton>
{rowDataStatic.trigger_on_checkin_after_time > 0 &&
<MythicStyledTooltip title={`Alert on callback after no checkin for ${rowDataStatic.trigger_on_checkin_after_time} minutes`}>
<NotificationsActiveTwoToneIcon color={"success"}/>
</MythicStyledTooltip>
}
{rowDataStatic.callbackports.length > 0 &&
<MythicStyledTooltip title={proxyMessage()}>
<FontAwesomeIcon icon={faSocks} size="lg" style={{color: theme.palette.success.main}}/>
</MythicStyledTooltip>
}
</div>
)
},
@@ -100,14 +147,15 @@ export const CallbacksTableStringCell = React.memo(({rowData, cellData}) => {
<div>{cellData}</div>
)
}, areEqual)
export const CallbacksTableLastCheckinCell = React.memo( ({rowData, cellData}) => {
export const CallbacksTableLastCheckinCell = React.memo( ({rowData, cellData, me}) => {
const adjustOutput = (newTime) => {
if(newTime === "a few seconds"){
moment.relativeTimeThreshold('s', 60);
moment.relativeTimeThreshold('ss', 0);
return moment(rowData.last_checkin + "Z", "YYYY-MM-DDTHH:mm:ss.SSSSSSZ").fromNow(true)
return moment(rowData.last_checkin + "Z", "YYYY-MM-DDTHH:mm:ss.SSSSSSZ").subtract(me?.user?.server_skew || 0, 'millisecond').fromNow(true)
}
return newTime;
return moment(rowData.last_checkin + "Z", "YYYY-MM-DDTHH:mm:ss.SSSSSSZ").subtract(me?.user?.server_skew || 0, 'millisecond').fromNow(true);
//return newTime;
}
const theme = useTheme();
if(rowData?.payload?.payloadtype?.agent_type !== "agent"){
@@ -201,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);}}
@@ -426,5 +474,13 @@ export const CallbacksTableSleepCell = React.memo( ({rowData, cellData, updateSl
</div>
)
}, areEqual)
export const CallbacksTableTagsCell = React.memo(({rowData, cellData}) => {
return (
<div>
<ViewEditTags target_object={"callback_id"} target_object_id={rowData.id} />
<TagsDisplay tags={cellData} expand={false} />
</div>
)
}, areEqual)
@@ -1,19 +1,28 @@
import React, { useEffect } from 'react';
import AppBar from '@mui/material/AppBar';
import Tabs from '@mui/material/Tabs';
import {
DragDropContext,
Droppable,
} from "@hello-pangea/dnd";
import { CallbacksTabsTaskingLabel, CallbacksTabsTaskingPanel } from './CallbacksTabsTasking';
import { CallbacksTabsFileBrowserLabel, CallbacksTabsFileBrowserPanel } from './CallbacksTabsFileBrowser';
import { CallbacksTabsProcessBrowserLabel, CallbacksTabsProcessBrowserPanel } from './CallbacksTabsProcessBrowser';
import { CallbacksTabsTaskingSplitLabel, CallbacksTabsTaskingSplitPanel} from "./CallbacksTabsTaskingSplit";
import {CallbacksTabsTaskingConsoleLabel, CallbacksTabsTaskingConsolePanel} from "./CallbacksTabsTaskingConsole";
import {
CallbacksTabsCustomFileBasedBrowserLabel,
CallbacksTabsCustomFileBasedBrowserPanel
} from "./CallbacksTabsCustomFileBasedBrowser";
export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, setClickedTabId, onEditTabDescription, contextMenuOptions, me}) {
export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clickedTabId, setClickedTabId, onEditTabDescription, contextMenuOptions, me, newDataForTab, setNewDataForTab}) {
const mountedRef = React.useRef(true);
const [value, setValue] = React.useState(0);
const handleChange = (event, newValue) => {
setValue(newValue);
//console.log(newValue);
//console.log("clickedTab", newValue, openTabs[newValue]);
if(openTabs[newValue] === undefined){return}
localStorage.setItem('clickedTab', openTabs[newValue].tabID);
setClickedTabId(openTabs[newValue].tabID);
};
@@ -27,7 +36,9 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, s
if (index > 0) {
setClickedTabId(openTabs[index-1].tabID);
setValue(index - 1);
} else {
//console.log("closing index", index)
} else if(openTabs.length > 0) {
//console.log("closing index length > 0", index)
setClickedTabId(openTabs[0].tabID);
setValue(0);
}
@@ -35,104 +46,154 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, s
};
useEffect(() => {
//console.log(clickedTabId);
//console.log(clickedTabId, openTabs);
for (let i = 0; i < openTabs.length; i++) {
//console.log("openTabs[i]", i, openTabs[i]);
if (openTabs[i].tabID === clickedTabId) {
//console.log("seting value", i);
//console.log("setting value", i);
setValue(i);
return;
}
}
}, [clickedTabId, openTabs]);
const [collapseTaskRequest, setCollapseTaskRequest] = React.useState({})
const modifiedInteractContextMenuOptions = [...contextMenuOptions,
{
name: 'Collapse All Tasks',
click: ({event, index}) => {
setCollapseTaskRequest((prevState) => {
if(prevState[index] !== undefined){
prevState[index] += 1;
return {...prevState};
} else {
return { [index]: 0};
}
});
}
},]
return (
<div style={{width: "100%", display: 'flex', flexDirection: 'column', flexGrow: 1, height: "100%" }}>
<AppBar color='default' position='static' className={"no-box-shadow"}>
<Tabs
value={value}
onChange={handleChange}
indicatorColor='primary'
textColor='primary'
variant='scrollable'
scrollButtons='auto'
style={{ maxWidth: '100%', width: '100%' }}
aria-label='scrollable auto tabs example'>
{openTabs.map((tab, index) => {
switch (tab.tabType) {
case 'interact':
return (
<CallbacksTabsTaskingLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
selectedIndex={value}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
);
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>
<div style={{width: "100%", maxWidth: "100%", display: 'flex', flexDirection: 'column', flexGrow: 1, height: "100%" }}>
<AppBar color='default' position={"static"} style={{backgroundColor: "transparent"}} >
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="callback-tabs-list" direction={"horizontal"}>
{(provided) => (
<>
<Tabs
ref={provided.innerRef} {...provided.droppableProps}
value={value}
onChange={handleChange}
indicatorColor='primary'
textColor='primary'
variant='fullWidth'
scrollButtons={true}
style={{ }}
TabIndicatorProps={{style: {display: "none"}}}
aria-label='scrollable tabs'>
{openTabs.map((tab, index) => {
switch (tab.tabType) {
case 'interact':
return (
<CallbacksTabsTaskingLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
newDataForTab={newDataForTab}
selectedIndex={value}
onDragTab={onDragTab}
contextMenuOptions={modifiedInteractContextMenuOptions}
/>
);
case 'interactSplit':
return (
<CallbacksTabsTaskingSplitLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
newDataForTab={newDataForTab}
selectedIndex={value}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
)
case 'interactConsole':
return (
<CallbacksTabsTaskingConsoleLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
newDataForTab={newDataForTab}
selectedIndex={value}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
)
case 'customFileBasedBrowser':
return (
<CallbacksTabsCustomFileBasedBrowserLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType + tab?.customBrowser?.name}
tabInfo={tab}
index={index}
me={me}
newDataForTab={newDataForTab}
selectedIndex={value}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
);
case 'processBrowser':
return (
<CallbacksTabsProcessBrowserLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
newDataForTab={newDataForTab}
selectedIndex={value}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
);
case 'fileBrowser':
return (
<CallbacksTabsFileBrowserLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
newDataForTab={newDataForTab}
selectedIndex={value}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
);
default:
return null;
}
})}
{provided.placeholder}
</Tabs>
</>
)}
</Droppable>
</DragDropContext>
</AppBar>
{openTabs.map((tab, index) => {
@@ -149,9 +210,11 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, s
key={'tabpanel' + tab.tabID + tab.tabType}
onCloseTab={onCloseTabLocal}
tabInfo={tab}
setNewDataForTab={setNewDataForTab}
value={value}
index={index}
me={me}
collapseTaskRequest={collapseTaskRequest}
parentMountedRef={mountedRef}
/>
);
@@ -167,6 +230,7 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, s
key={'tabpanel' + tab.tabID + tab.tabType}
onCloseTab={onCloseTabLocal}
tabInfo={tab}
setNewDataForTab={setNewDataForTab}
value={value}
index={index}
me={me}
@@ -185,12 +249,32 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, s
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
@@ -203,10 +287,12 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, s
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':
@@ -221,6 +307,7 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, s
onCloseTab={onCloseTabLocal}
key={'tabpanel' + tab.tabID + tab.tabType}
tabInfo={tab}
setNewDataForTab={setNewDataForTab}
value={value}
index={index}
me={me}
@@ -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);
};
@@ -26,8 +26,10 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import {b64DecodeUnicode} from "./ResponseDisplay";
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
import PlaylistRemoveIcon from '@mui/icons-material/PlaylistRemove';
import {SetMythicSetting, useMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
import {useGetMythicSetting, useSetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
import {RenderSingleTask} from "../SingleTaskView/SingleTaskView";
import {loadedCommandsQuery} from "./CallbacksTabsProcessBrowser";
import {getSkewedNow} from "../../utilities/Time";
const fileDataFragment = gql`
fragment fileObjData on mythictree {
@@ -45,10 +47,12 @@ const fileDataFragment = gql`
color
id
}
id
}
host
id
can_have_children
has_children
success
full_path_text
name_text
@@ -66,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
}
}
@@ -171,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}
@@ -194,8 +198,8 @@ export function CallbacksTabsFileBrowserLabel(props) {
</React.Fragment>
);
}
export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) => {
const fromNow = React.useRef((new Date()));
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
// hold the simple adjacency matrix for parent/child relationships
@@ -213,13 +217,15 @@ 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: "",
group: "",
full_path_text: ""
});
const loadedCommandsRef = React.useRef({});
const loadingCommandsRef = React.useRef(false);
useQuery(rootFileQuery, {
onCompleted: (data) => {
// use an adjacency matrix but only for full_path_text -> children, not both directions
@@ -251,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
@@ -311,21 +321,34 @@ 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){
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)};
})
existingData.filemeta = [...existingData.filemeta, ...newfileData]
existingData.filemeta.sort((a,b) => a.id > b.id ? 1 : -1);
treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]] = {...existingData};
if(selectedFolderData.group === currentGroups[j] && selectedFolderData.host === data.data.mythictree_stream[i]["host"] &&
selectedFolderData.full_path_text === data.data.mythictree_stream[i]["full_path_text"]){
@@ -353,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, {
@@ -362,12 +387,46 @@ 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});
}
}
}
})
const [getLoadedCommandsQuery] = useLazyQuery(loadedCommandsQuery, {
fetchPolicy: 'no-cache',
onCompleted: (data) => {
if(data.loadedcommands.length > 0){
loadedCommandsRef.current[data.loadedcommands[0].callback_id] = [...data.loadedcommands];
}
loadingCommandsRef.current = false;
},
onError: (data) => {
console.log(data);
loadingCommandsRef.current = false;
}
});
async function getLoadedCommandForUIFeature (callback_id, uifeature){
while(loadingCommandsRef.current){
await new Promise(resolve => setTimeout(resolve, 50));
}
if(loadedCommandsRef.current[callback_id] === undefined){
loadingCommandsRef.current = true;
getLoadedCommandsQuery({variables: {callback_id: callback_id}});
while(loadingCommandsRef.current){
await new Promise(resolve => setTimeout(resolve, 50));
}
}
if(loadedCommandsRef.current[callback_id] === undefined){
return undefined;
}
for(let i = 0; i < loadedCommandsRef.current[callback_id].length; i++){
if(loadedCommandsRef.current[callback_id][i].command.supported_ui_features.includes(uifeature)){
return loadedCommandsRef.current[callback_id][i];
}
}
return undefined;
}
const [getFolderData] = useLazyQuery(folderQuery, {
onError: (data) => {
console.error(data);
@@ -403,20 +462,35 @@ 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){
existingData.deleted = mythictree[i].deleted;
}
if( (existingData.success === null || !existingData.success) && mythictree[i].success !== null){
existingData.success = mythictree[i].success;
existingData.task_id = mythictree[i].task_id;
}
existingData.comment += mythictree[i].comment;
existingData.tags = [...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)};
})
existingData.filemeta = [...existingData.filemeta, ...newfileData]
existingData.filemeta.sort((a,b) => a.id > b.id ? 1 : -1);
treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]][mythictree[i]["full_path_text"]] = {...existingData};
if(selectedFolderData.group === currentGroups[j] && selectedFolderData.host === mythictree[i]["host"] &&
selectedFolderData.full_path_text === mythictree[i]["full_path_text"]){
@@ -509,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 = () => {
@@ -521,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);
@@ -530,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);
@@ -589,8 +663,8 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
const taskListing = (nodeData, callback_id, callback_display_id) => {
taskingData.current = ({
"token": localSelectedToken.current,
"parameters": {path: nodeData.full_path_text, full_path: nodeData.full_path_text, host: selectedFolderData.host, file: ""},
"ui_feature": "file_browser:list", callback_id, callback_display_id});
"parameters": {path: nodeData.full_path_text, full_path: nodeData.full_path_text, host: nodeData.host, file: ""},
"ui_feature": baseUIFeature + ":list", callback_id, callback_display_id});
setOpenTaskingButton(true);
}
const openDirectoryPath = ({group, host, path}) => {
@@ -623,6 +697,8 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
setTableData={onSetTableData}
taskListing={taskListing}
tableOpenedPathId={tableOpenedPathIdRef.current}
getLoadedCommandForUIFeature={getLoadedCommandForUIFeature}
baseUIFeature={baseUIFeature}
/>
</div>
@@ -641,6 +717,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
fetchParentFolderData={fetchParentFolderData}
fetchFolderData={fetchFolderData}
openDirectoryPath={openDirectoryPath}
baseUIFeature={baseUIFeature}
/>
</div>
<div style={{ flexGrow: 1 }}>
@@ -658,7 +735,9 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
autoTaskLsOnEmptyDirectories={autoTaskLsOnEmptyDirectoriesRef.current}
onTaskRowAction={onTaskRowAction}
onTaskRowActions={onTaskRowActions}
getLoadedCommandForUIFeature={getLoadedCommandForUIFeature}
me={me}
baseUIFeature={baseUIFeature}
/>
</div>
</div>
@@ -690,12 +769,13 @@ const FileBrowserTableTop = ({
fetchParentFolderData,
fetchFolderData,
openDirectoryPath,
tabInfo
tabInfo,
baseUIFeature
}) => {
const initialAutoTaskLsOnEmptyDirectories = useMythicSetting({
setting_name: "autoTaskLsOnEmptyDirectories", default_value: "false"
const autoTaskLsOnEmptyDirectories = useGetMythicSetting({
setting_name: "autoTaskLsOnEmptyDirectories", default_value: false
});
const [autoTaskLsOnEmptyDirectories, setAutoTaskLsOnEmptyDirectories] = React.useState(initialAutoTaskLsOnEmptyDirectories);
const [updateMythicSetting] = useSetMythicSetting();
const [fullPath, setFullPath] = React.useState('');
const selectedToken = React.useRef("Default Token");
const [tokenOptions, setTokenOptions] = React.useState([]);
@@ -726,6 +806,9 @@ const FileBrowserTableTop = ({
shouldResubscribe: true,
onData: ({data}) => {
setTokenOptions(data.data.callbacktoken);
if(data.data.callbacktoken.length === 0) {
onChangeSelectedToken("Default Token");
}
}
});
useEffect(() => {
@@ -768,7 +851,7 @@ const FileBrowserTableTop = ({
}
const moveIndexToNextListing = () => {
// we're getting close to index 0, the newest listing
console.log(historyIndex, history);
//console.log(historyIndex, history);
if(historyIndex <= 0){
return
}
@@ -803,8 +886,7 @@ const FileBrowserTableTop = ({
}
};
const onToggleAutoTaskLsOnEmptyDirectories = () => {
SetMythicSetting({setting_name: "autoTaskLsOnEmptyDirectories", value: !autoTaskLsOnEmptyDirectories});
setAutoTaskLsOnEmptyDirectories(!autoTaskLsOnEmptyDirectories);
updateMythicSetting({setting_name: "autoTaskLsOnEmptyDirectories", value: !autoTaskLsOnEmptyDirectories});
if(autoTaskLsOnEmptyDirectories){
snackActions.info("No longer auto issuing listings for empty paths");
} else {
@@ -836,7 +918,7 @@ const FileBrowserTableTop = ({
}, [autoTaskLsOnEmptyDirectories]);
return (
<Grid container spacing={0} style={{ paddingTop: '10px' }}>
<Grid item xs={12} >
<Grid size={12}>
<MythicTextField
placeholder={selectedFolderData.host}
value={fullPath}
@@ -897,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,6 +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,
GetMythicSetting,
useSetMythicSetting
} from "../../MythicComponents/MythicSavedUserSetting";
import {CallbacksTableColumnsReorderDialog} from "./CallbacksTableColumnsReorderDialog";
const getFileDownloadHistory = gql`
query getFileDownloadHistory($full_path_text: String!, $host: String!, $group: [String!]) {
@@ -51,6 +55,7 @@ const getFileDownloadHistory = gql`
timestamp
filename_text
host
deleted
full_remote_path_text
task {
id
@@ -74,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}];
@@ -106,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) {
@@ -186,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;
}
@@ -214,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} />;
@@ -222,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}
@@ -252,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";
@@ -302,8 +324,12 @@ export const CallbacksTabsFileBrowserTable = (props) => {
};
const contextMenuOptions = [
{
name: 'Filter Column',
name: 'Filter Column', type: "item", icon: null,
click: ({event, columnIndex}) => {
if(event){
event.stopPropagation();
event.preventDefault();
}
if(columns[columnIndex].disableFilterMenu){
snackActions.warning("Can't filter that column");
return;
@@ -313,14 +339,84 @@ export const CallbacksTabsFileBrowserTable = (props) => {
}
},
{
name: "Show/Hide Columns",
name: "Reorder Columns and Adjust Visibility",type: "item", icon: null,
click: ({event, columnIndex}) => {
setOpenAdjustColumnsDialog(true);
if(event){
event.stopPropagation();
event.preventDefault();
}
setOpenReorderDialog(true);
}
}
];
const onSubmitAdjustColumns = ({left, right}) => {
setColumnVisibility({visible: right, hidden: left});
React.useEffect( () => {
try {
const storageItem = GetMythicSetting({setting_name: `file_browser_table_columns`, default_value: defaultVisibleColumns});
if(storageItem !== null){
let allColumns = [...columnVisibility["visible"].map(c => c), ...columnVisibility["hidden"].map(c => c)];
let newHidden = [];
allColumns.forEach((v,i,a) => {
if(!storageItem.includes(v)){
newHidden.push(v);
}
});
if(storageItem.length !== 0){
setColumnVisibility({visible: storageItem, hidden: newHidden});
}
}
}catch(error){
console.log("Failed to load custom browser_table_columns", error);
}
try {
const storageItemOptions = GetMythicSetting({setting_name: `file_browser_filter_options`, default_value: {}});
if(storageItemOptions !== null){
setFilterOptions(storageItemOptions);
}
}catch(error){
console.log("Failed to load custom browser_table_filter_options", error);
}
try {
const storageColumnOrder = GetMythicSetting({setting_name: `file_browser_column_order`, default_value: columns.map(c => c.name)});
if(storageColumnOrder !== null){
let newOrder = [];
for(let i = 0; i < storageColumnOrder.length; i++){
for(let j = 0; j < columnOrder.length; j++){
if(columnOrder[j].name === storageColumnOrder[i]){
newOrder.push(columnOrder[j]);
break;
}
}
}
setColumnOrder(newOrder);
}
}catch(error){
console.log("Failed to load file_browser_table_filter_options", error);
}
setLoading(false);
}, []);
const onSubmitColumnReorder = (newOrder) => {
let newVisible = [];
let newHidden = [];
for(let i = 0; i < newOrder.length; i++){
if(newOrder[i].visible){
newVisible.push(newOrder[i].name);
} else {
newHidden.push(newOrder[i].name);
}
}
if(newVisible.length === 0){
snackActions.error("Can't update to show no fields");
return;
}
console.log("newOrder", newOrder)
updateSetting({setting_name: `file_browser_column_order`, value: newOrder.map(c => c.name)});
setColumnOrder(newOrder);
setColumnVisibility({visible: newVisible, hidden: newHidden});
updateSetting({setting_name: `file_browser_table_columns`, value: newVisible});
setOpenReorderDialog(false);
}
const onResetColumnReorder = () => {
onSubmitColumnReorder(columnDefaults);
}
const sortColumn = columns.findIndex((column) => column.key === sortData.sortKey);
const onRowClick = ({event, rowDataStatic}) => {
@@ -342,13 +438,132 @@ export const CallbacksTabsFileBrowserTable = (props) => {
setSelectedRows([rowDataStatic]);
}
}
const onRowContextClick = ({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?.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 ? 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, 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 [
{
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: element.can_have_children ? props.baseUIFeature + ':remove_folder' : props.baseUIFeature + ':remove',
getConfirmation: true,
callback_id, callback_display_id
});
},
},
];
}
async function onRowContextClick({rowDataStatic}) {
// based on row, return updated options array?
if(selectedRows.length > 1){
return [
{
name: `Download All Selected`, type: "item",
disabled: rowDataStatic.can_have_children,
icon: <GetAppIcon color="success" style={{ paddingRight: '5px' }} />,
click: ({event}) => {
event.stopPropagation();
@@ -359,7 +574,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
full_path: selectedRows[i].full_path_text,
host: selectedRows[i].host,
filename: selectedRows[i].name_text,
uifeature: '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
});
@@ -368,7 +583,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
},
},
{
name: `Delete All Selected`, type: "item",
name: `Remove All Selected`, type: "item",
icon: <DeleteIcon color="error" style={{ paddingRight: '5px' }} />,
click: ({event}) => {
event.stopPropagation();
@@ -379,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
@@ -390,7 +605,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
},
{
name: `List All Selected`, type: "item",
icon: <ListIcon color="error" style={{ paddingRight: '5px' }} />,
icon: <ListIcon color="warning" style={{ paddingRight: '5px' }} />,
click: ({event}) => {
event.stopPropagation();
let newTasks = [];
@@ -400,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
});
@@ -410,146 +625,32 @@ export const CallbacksTabsFileBrowserTable = (props) => {
},
]
}
return [
{
name: rowDataStatic.name_text, icon: null, click: ({event}) => {},
type: "item", disabled: true
},
{
name: `Download`, type: "item",
disabled: rowDataStatic.can_have_children,
icon: <GetAppIcon color="success" style={{ paddingRight: '5px' }} />,
click: ({event}) => {
event.stopPropagation();
props.onTaskRowAction({
path: rowDataStatic.parent_path_text,
full_path: rowDataStatic.full_path_text,
host: rowDataStatic.host,
filename: rowDataStatic.name_text,
uifeature: 'file_browser:download',
callback_id: props.tabInfo.callbackID,
callback_display_id: props.tabInfo.displayID
});
},
},
{
name: `List`, type: "item",
icon: <ListIcon color="warning" style={{ paddingRight: '5px'}} />,
click: ({event}) => {
event.stopPropagation();
props.onTaskRowAction({
path: rowDataStatic.parent_path_text,
full_path: rowDataStatic.full_path_text,
host: rowDataStatic.host,
filename: rowDataStatic.name_text,
uifeature: 'file_browser:list',
callback_id: props.tabInfo.callbackID,
callback_display_id: props.tabInfo.displayID
});
},
},
{
name: `Delete`, type: "item",
icon: <DeleteIcon color="error" style={{ paddingRight: '5px' }} />,
click: ({event}) => {
event.stopPropagation();
props.onTaskRowAction({
path: rowDataStatic.parent_path_text,
full_path: rowDataStatic.full_path_text,
host: rowDataStatic.host,
filename: rowDataStatic.name_text,
uifeature: 'file_browser:remove',
getConfirmation: true,
callback_id: props.tabInfo.callbackID,
callback_display_id: props.tabInfo.displayID
});
},
},
{
name: "Copy to Clipboard", icon: null, click: () => {}, type: "menu",
let options = [...optionsA(rowDataStatic)];
options.push(...(await optionsB(props.tabInfo.callbackID, props.tabInfo.displayID, rowDataStatic)));
if(rowDataStatic.callback.display_id !== props.tabInfo.displayID){
options.push({
name: `Original Callback: ${rowDataStatic.callback.display_id}`, icon: null, click: () => {}, type: "menu",
menuItems: [
{
name: 'Name', type: "item",
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
click: ({event}) => {
event.stopPropagation();
copyStringToClipboard(rowDataStatic.name_text);
},
},
{
name: 'Full Path', type: "item",
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
click: ({event}) => {
event.stopPropagation();
copyStringToClipboard(rowDataStatic.full_path_text);
},
},
{
name: 'Attributes', type: "item",
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
click: ({event}) => {
event.stopPropagation();
copyStringToClipboard(JSON.stringify(rowDataStatic?.metadata?.permissions, null, 2));
},
},
...(await optionsB(rowDataStatic.callback.id, rowDataStatic.callback.display_id, rowDataStatic))
]
},
{
name: "Original Callback: " + rowDataStatic.callback.display_id, icon: null,
click: () => {}, type: "menu",
menuItems: [
{
name: 'Task Listing', type: "item",
icon: <ListIcon color="warning" style={{ paddingRight: '5px'}} />,
click: ({event}) => {
event.stopPropagation();
props.onTaskRowAction({
path: rowDataStatic.parent_path_text,
full_path: rowDataStatic.full_path_text,
host: rowDataStatic.host,
filename: rowDataStatic.name_text,
uifeature: 'file_browser:list',
callback_id: rowDataStatic.callback.id,
callback_display_id: rowDataStatic.callback.display_id
});
},
},
{
name: 'Task Download', type: "item",
icon: <GetAppIcon color="success" style={{ paddingRight: '5px' }} />,
click: ({event}) => {
event.stopPropagation();
props.onTaskRowAction({
path: rowDataStatic.parent_path_text,
full_path: rowDataStatic.full_path_text,
host: rowDataStatic.host,
filename: rowDataStatic.name_text,
uifeature: 'file_browser:download',
callback_id: rowDataStatic.callback.id,
callback_display_id: rowDataStatic.callback.display_id
});
},
},
{
name: 'Task Removal', type: "item",
icon: <DeleteIcon color="error" style={{ paddingRight: '5px' }} />,
click: ({event}) => {
event.stopPropagation();
props.onTaskRowAction({
path: rowDataStatic.parent_path_text,
full_path: rowDataStatic.full_path_text,
host: rowDataStatic.host,
filename: rowDataStatic.name_text,
uifeature: 'file_browser:remove',
getConfirmation: true,
callback_id: rowDataStatic.callback.id,
callback_display_id: rowDataStatic.callback.display_id
});
},
},
]
},
];
})
}
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"}}>
@@ -559,7 +660,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
sortIndicatorIndex={sortColumn}
sortDirection={sortData.sortDirection}
items={gridData}
rowHeight={20}
rowHeight={GetComputedFontSize() + 7}
onClickHeader={onClickHeader}
onDoubleClickRow={onRowDoubleClick}
contextMenuOptions={contextMenuOptions}
@@ -631,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>
);
};
@@ -658,22 +767,19 @@ 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 ? (
<DescriptionIcon style={{
width: "15px",
height: "15px",
<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={"lg"}
size={"1x"}
style={{
width: "15px",
height: "15px",
marginRight: '5px',
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',
}}
@@ -682,18 +788,18 @@ 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 ? (
<MythicStyledTooltip title='Successfully listed contents of folder' style={{display: "inline-flex", marginLeft: "5px"}}>
{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 ? (
<MythicStyledTooltip title='Failed to list contents of folder' style={{display: "inline-flex", marginLeft: "5px"}}>
) : 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}
@@ -772,7 +878,7 @@ export const TableRowSizeCell = ({ cellData, rowData }) => {
};
return getStringSize(cellData);
};
const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, treeRootData, selectedFolderData, me, tabInfo }) => {
const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, treeRootData, selectedFolderData, me, tabInfo, getLoadedCommandForUIFeature }) => {
const dropdownAnchorRef = React.useRef(null);
const [dropdownOpen, setDropdownOpen] = React.useState(false);
const [fileCommentDialogOpen, setFileCommentDialogOpen] = React.useState(false);
@@ -813,11 +919,6 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
const onSubmitUpdatedComment = (comment) => {
updateComment({ variables: { mythictree_id: treeRootData[selectedFolderData.host][cellData].id, comment: comment } });
};
const handleDropdownToggle = (evt) => {
evt.stopPropagation();
evt.preventDefault();
setDropdownOpen((prevOpen) => !prevOpen);
};
const handleMenuItemClick = (event, click) => {
click({event});
setDropdownOpen(false);
@@ -829,12 +930,16 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
setDropdownOpen(false);
};
const optionsA = [
{
name: treeRootData[selectedFolderData.host][rowData.full_path_text]?.name_text, icon: null, click: ({event}) => {},
type: "item", disabled: true
},
{
name: 'View Permissions', type: "item",
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);
},
},
@@ -844,7 +949,7 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
click: ({event}) => {
event.stopPropagation();
getHistory({ variables: {
full_path_text: treeRootData[selectedFolderData.host][cellData].full_path_text,
full_path_text: treeRootData[selectedFolderData.host][rowData.full_path_text].full_path_text,
host: selectedFolderData.host,
group: [selectedFolderData.group],
} });
@@ -859,67 +964,11 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
},
},
];
const optionsB = (callback_id, callback_display_id) => [
{
name: 'List', type: "item",
icon: <ListIcon color="warning" style={{ paddingRight: '5px'}} />,
click: ({event}) => {
event.stopPropagation();
onTaskRowAction({
path: treeRootData[selectedFolderData.host][cellData].parent_path_text,
full_path: treeRootData[selectedFolderData.host][cellData].full_path_text,
host: treeRootData[selectedFolderData.host][cellData].host,
filename: treeRootData[selectedFolderData.host][cellData].name_text,
uifeature: 'file_browser:list',
callback_id, callback_display_id
});
},
},
{
name: 'Download', type: "item",
icon: <GetAppIcon color="success" style={{ paddingRight: '5px' }} />,
click: ({event}) => {
event.stopPropagation();
onTaskRowAction({
path: treeRootData[selectedFolderData.host][cellData].parent_path_text,
full_path: treeRootData[selectedFolderData.host][cellData].full_path_text,
host: treeRootData[selectedFolderData.host][cellData].host,
filename: treeRootData[selectedFolderData.host][cellData].name_text,
uifeature: 'file_browser:download',
callback_id, callback_display_id
});
},
},
{
name: 'Delete', type: "item",
icon: <DeleteIcon color="error" style={{ paddingRight: '5px' }} />,
click: ({event}) => {
event.stopPropagation();
onTaskRowAction({
path: treeRootData[selectedFolderData.host][cellData].parent_path_text,
full_path: treeRootData[selectedFolderData.host][cellData].full_path_text,
host: treeRootData[selectedFolderData.host][cellData].host,
filename: treeRootData[selectedFolderData.host][cellData].name_text,
uifeature: 'file_browser:remove',
getConfirmation: true,
callback_id, callback_display_id
});
},
},
];
const getMenuOptions = () => {
let options = [...optionsA];
options.push(...optionsB(tabInfo["callbackID"], tabInfo["displayID"]));
if(rowData?.callback?.["display_id"] !== tabInfo["displayID"]){
options.push({
name: "Original Callback", icon: null, click: () => {}, type: "menu",
menuItems: [
...optionsB(rowData?.callback?.id, rowData?.callback?.display_id)
]
})
}
return options;
}
const handleDropdownToggle = (evt) => {
evt.stopPropagation();
setDropdownOpen((prevOpen) => !prevOpen);
};
const openFilePreview = (event) => {
event.stopPropagation();
event.preventDefault();
@@ -939,10 +988,10 @@ 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} style={{height: "15px", marginRight: "5px",
<FontAwesomeIcon icon={faPhotoVideo} size={"1x"} style={{
position: "relative", cursor: "pointer", display: "inline-block"}}
onClick={openFilePreview}/>
</MythicStyledTooltip>
@@ -952,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={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);}} />}
/>
}
@@ -964,7 +1013,7 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
onOpen={setDropdownOpen}
externallyOpen={dropdownOpen}
menu={[
...getMenuOptions().map((option, index) => (
...optionsA.map((option, index) => (
option.type === 'item' ? (
<DropdownMenuItem
key={option.name}
@@ -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}) => {
@@ -106,10 +144,10 @@ export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatri
options.push(...contextMenuOptions(tabInfo["callbackID"], tabInfo["displayID"], contextMenuData.current?.item));
if(contextMenuData.current?.item?.callback?.["display_id"] !== tabInfo["displayID"]){
options.push({
name: "Original Callback " + contextMenuData.current?.item?.callback?.["display_id"],
name: "Original Callback: " + contextMenuData.current?.item?.callback?.["display_id"],
icon: null, click: () => {}, type: "menu",
menuItems: [
...contextMenuOptions(contextMenuData.current?.item?.callback?.id, contextMenuData.current?.item?.callback?.display_id)
...contextMenuOptions(contextMenuData.current?.item?.callback?.id, contextMenuData.current?.item?.callback?.display_id, contextMenuData.current?.item)
]
})
}
@@ -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`
@@ -63,11 +64,11 @@ const columns = [
width: 100,
renderCell: (params) => <CallbacksTableIPCell rowData={params.row} cellData={params.row.ip} />,
sortable: false,
valueGetter: (params) => {
valueGetter: (value, row) => {
try{
return JSON.parse(params.row.ip)[0];
return JSON.parse(row.ip)[0];
}catch(error){
return params.row.ip;
return row.ip;
}
}
},
@@ -75,7 +76,7 @@ const columns = [
field: "last_checkin",
headerName: "Checkin",
width: 100,
valueGetter: (params) => new Date(params.row.last_checkin),
valueGetter: (value, row) => new Date(row.last_checkin),
renderCell: (params) =>
<CallbacksTableLastCheckinCell rowData={params.row} />,
},
@@ -83,7 +84,7 @@ const columns = [
field: "payload.payloadtype.name",
headerName: "Agent",
flex: 0.5,
valueGetter: (params) => params.row.payload.payloadtype.name,
valueGetter: (value, row) => row.payload.payloadtype.name,
renderCell: (params) => <CallbacksTablePayloadTypeCell rowData={params.row} />
},
{
@@ -92,12 +93,15 @@ const columns = [
flex: 0.5,
}
];
const CustomSelectTable = ({initialData, selectedData}) => {
const CustomSelectTable = ({initialData, selectedData, sortModel}) => {
const [data, setData] = React.useState([]);
const [rowSelectionModel, setRowSelectionModel] = React.useState([]);
const [rowSelectionModel, setRowSelectionModel] = React.useState({
type: 'include',
ids: new Set([]),
});
React.useEffect( () => {
selectedData.current = data.reduce( (prev, cur) => {
if(rowSelectionModel.includes(cur.id)){return [...prev, cur]}
if(rowSelectionModel.ids.has(cur.id)){return [...prev, cur]}
return [...prev];
}, []);
}, [data, rowSelectionModel]);
@@ -117,7 +121,7 @@ const CustomSelectTable = ({initialData, selectedData}) => {
},
},
sorting: {
sortModel: [{ field: 'last_checkin', sort: 'asc' }],
sortModel: [sortModel],
},
}}
autoPageSize
@@ -172,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
@@ -185,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`
@@ -60,11 +61,11 @@ const columns = [
width: 100,
renderCell: (params) => <CallbacksTableIPCell rowData={params.row} cellData={params.row.ip} />,
sortable: false,
valueGetter: (params) => {
valueGetter: (value, row) => {
try{
return JSON.parse(params.row.ip)[0];
return JSON.parse(row.ip)[0];
}catch(error){
return params.row.ip;
return row.ip;
}
}
},
@@ -72,7 +73,7 @@ const columns = [
field: "last_checkin",
headerName: "Checkin",
width: 100,
valueGetter: (params) => new Date(params.row.last_checkin),
valueGetter: (value, row) => new Date(row.last_checkin),
renderCell: (params) =>
<CallbacksTableLastCheckinCell rowData={params.row} />,
},
@@ -80,7 +81,7 @@ const columns = [
field: "payload.payloadtype.name",
headerName: "Agent",
flex: 0.5,
valueGetter: (params) => params.row.payload.payloadtype.name,
valueGetter: (value, row) => row.payload.payloadtype.name,
renderCell: (params) => <CallbacksTablePayloadTypeCell rowData={params.row} />
},
{
@@ -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';
@@ -23,6 +21,7 @@ import {snackActions} from "../../utilities/Snackbar";
import { Backdrop, Typography } from '@mui/material';
import {CircularProgress} from '@mui/material';
import ExpandIcon from '@mui/icons-material/Expand';
import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery";
const treeFragment = gql`
fragment treeObjData on mythictree {
@@ -61,7 +60,7 @@ const treeSubscription = gql`
mythictree_stream(
batch_size: 1000,
cursor: {initial_value: {timestamp: $now}},
where: {tree_type: {_eq: "process"} }
where: {tree_type: {_eq: "process"}, deleted: {_eq: false} }
) {
...treeObjData
}
@@ -78,11 +77,29 @@ const rootQuery = gql`
const treeHostQuery = gql`
${treeFragment}
query processesPerHostQuery($host: String!){
mythictree(where: {host: {_eq: $host}, tree_type: {_eq: "process"} }, order_by: {id: asc}) {
mythictree(where: {host: {_eq: $host}, tree_type: {_eq: "process"}, deleted: {_eq: false} }, order_by: {id: asc}) {
...treeObjData
}
}
`;
export const loadedCommandsQuery = gql`
query loadedCommandUIFeatures($callback_id: Int!){
loadedcommands(where: {callback_id: {_eq: $callback_id}}){
callback_id
command {
supported_ui_features
cmd
id
payloadtype {
name
id
}
}
id
version
}
}
`;
export function CallbacksTabsProcessBrowserLabel(props){
const [description, setDescription] = React.useState("Processes: " + props.tabInfo.displayID)
const [openEditDescriptionDialog, setOpenEditDescriptionDialog] = React.useState(false);
@@ -113,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={
@@ -124,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);
@@ -136,12 +153,14 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>
const [showDeletedFiles, setShowDeletedFiles] = React.useState(false);
const [selectedHost, setSelectedHost] = React.useState("");
const [selectedGroup, setSelectedGroup] = React.useState("");
const loadedCommandsRef = React.useRef({});
const loadingCommandsRef = React.useRef(false);
const getNewMatrix = () => {
let newMatrix = {};
for(const[group, groupMatrix] of Object.entries(treeRootDataRef.current)){
if(newMatrix[group] === undefined){newMatrix[group] = {}}
for(const[host, hostMatrix] of Object.entries(treeRootDataRef.current[group])){
if(newMatrix[group][host] === undefined){newMatrix[group][host] = {"":[]}}
if(newMatrix[group][host] === undefined){newMatrix[group][host] = {"":{}}}
for(const[full_path_text, node] of Object.entries(treeRootDataRef.current[group][host])){
if(newMatrix[group][host][ node["parent_path_text"] ] === undefined){
newMatrix[group][host][ node["parent_path_text"] ] = {}
@@ -216,6 +235,19 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>
},
fetchPolicy: 'no-cache',
});
const getLoadedCommandsQuerySuccess = (data) => {
if(data.loadedcommands.length > 0){
loadedCommandsRef.current[data.loadedcommands[0].callback_id] = [...data.loadedcommands];
}
loadingCommandsRef.current = false;
}
const getLoadedCommandsQueryError = (data) => {
console.log(data);
loadingCommandsRef.current = false;
}
const getLoadedCommandsQuery = useMythicLazyQuery(loadedCommandsQuery, {
fetchPolicy: 'no-cache',
});
useSubscription(treeSubscription, {
variables: {now: fromNow.current},
fetchPolicy: "no-cache",
@@ -232,67 +264,83 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>
}
if(treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]] === undefined){
// first time we're seeing this file data, just add it
if(data.data.mythictree_stream[i].deleted){continue}
treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]] = {...data.data.mythictree_stream[i]};
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
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];
existingData.metadata = {...existingData.metadata, ...data.data.mythictree_stream[i].metadata};
if(existingData.task_id > data.data.mythictree_stream[i].task_id){
existingData.metadata = {...data.data.mythictree_stream[i].metadata, ...existingData.metadata};
} else {
existingData.metadata = {...existingData.metadata, ...data.data.mythictree_stream[i].metadata};
}
treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]] = {...existingData};
}
}
}
const newMatrix = getNewMatrix();
setTreeAdjMtx(newMatrix);
if(index !== value){
setNewDataForTab((prev) => {return {...prev, [tabInfo.tabID]: true}});
}
}
})
const [getHostProcessesQuery] = useLazyQuery(treeHostQuery, {
onError: (data) => {
console.error(data);
},
fetchPolicy: 'no-cache',
onCompleted: (data) => {
snackActions.dismiss();
// add in all of the raw data
for(let i = 0; i < data.mythictree.length; i++){
let currentGroups = data.mythictree[i]?.["callback"]?.["mythictree_groups"] || ["Unknown Callbacks"];
for(let j = 0; j < currentGroups.length; j++){
if(treeRootDataRef.current[currentGroups[j]] === undefined){
treeRootDataRef.current[currentGroups[j]] = {};
});
const getHostProcessesQuerySuccess = (data) => {
snackActions.dismiss();
// add in all of the raw data
for(let i = 0; i < data.mythictree.length; i++){
let currentGroups = data.mythictree[i]?.["callback"]?.["mythictree_groups"] || ["Unknown Callbacks"];
for(let j = 0; j < currentGroups.length; j++){
if(treeRootDataRef.current[currentGroups[j]] === undefined){
treeRootDataRef.current[currentGroups[j]] = {};
}
if( treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]] === undefined) {
// new host discovered
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]] = {};
}
if(treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] === undefined){
// first time we're seeing this file data, just add it
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] = {...data.mythictree[i]};
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]].callbacks = [data.mythictree[i].callback];
} else {
// we need to merge data in because we already have some info
if(data.mythictree[i].deleted){
delete treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]];
continue;
}
if( treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]] === undefined) {
// new host discovered
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]] = {};
}
if(treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] === undefined){
// first time we're seeing this file data, just add it
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] = {...data.mythictree[i]};
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]].callbacks = [data.mythictree[i].callback];
let existingData = treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]];
existingData.comment += data.mythictree[i].comment;
existingData.callbacks.push(data.mythictree[i].callback)
existingData.tags = [...existingData.tags, ...data.mythictree[i].tags];
if(existingData.task_id > data.mythictree[i].task_id){
existingData.metadata = {...data.mythictree[i].metadata, ...existingData.metadata};
} else {
// we need to merge data in because we already have some info
let existingData = treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]];
existingData.comment += data.mythictree[i].comment;
existingData.callbacks.push(data.mythictree[i].callback)
existingData.tags = [...existingData.tags, ...data.mythictree[i].tags];
if(existingData.id > data.mythictree[i].id){
existingData.metadata = {...existingData.metadata};
} else {
existingData.metadata = {...data.mythictree[i].metadata};
}
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] = {...existingData};
existingData.metadata = {...existingData.metadata, ...data.mythictree[i].metadata};
}
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] = {...existingData};
}
}
// create the top level data in the adjacency matrix
const newMatrix = getNewMatrix();
//console.log(treeRootDataRef.current);
setTreeAdjMtx(newMatrix);
//console.log("just set treeAdjMtx, about to close backdrop")
setBackdropOpen(false);
}
// create the top level data in the adjacency matrix
const newMatrix = getNewMatrix();
//console.log(treeRootDataRef.current);
setTreeAdjMtx(newMatrix);
//console.log("just set treeAdjMtx, about to close backdrop")
setBackdropOpen(false);
}
const getHostProcessesQuery = useMythicLazyQuery(treeHostQuery, {
fetchPolicy: 'no-cache',
})
const onListFilesButton = () => {
taskingData.current = ({parameters: "",
@@ -321,8 +369,31 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>
setSelectedHost("");
}
}
async function getLoadedCommandForUIFeature (callback_id, uifeature){
while(loadingCommandsRef.current){
await new Promise(resolve => setTimeout(resolve, 50));
}
if(loadedCommandsRef.current[callback_id] === undefined){
loadingCommandsRef.current = true;
getLoadedCommandsQuery({variables: {callback_id: callback_id}})
.then(({data}) => getLoadedCommandsQuerySuccess(data)).catch(({data}) => getLoadedCommandsQueryError(data));
while(loadingCommandsRef.current){
await new Promise(resolve => setTimeout(resolve, 50));
}
}
if(loadedCommandsRef.current[callback_id] === undefined){
return undefined;
}
for(let i = 0; i < loadedCommandsRef.current[callback_id].length; i++){
if(loadedCommandsRef.current[callback_id][i].command.supported_ui_features.includes(uifeature)){
return loadedCommandsRef.current[callback_id][i];
}
}
return undefined;
}
React.useEffect( () => {
getHostProcessesQuery({variables: {host: selectedHost}});
getHostProcessesQuery({variables: {host: selectedHost}})
.then(({data}) => getHostProcessesQuerySuccess(data)).catch(({data}) => console.log(data));
setBackdropOpen(true);
}, [selectedHost]);
React.useEffect( () => {
@@ -373,6 +444,7 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>
group={selectedGroup}
expandOrCollapseAll={expandOrCollapseAll}
onTaskRowAction={onTaskRowAction}
getLoadedCommandForUIFeature={getLoadedCommandForUIFeature}
me={me}/>
</div>
@@ -383,6 +455,7 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>
openDialog={taskingData.current?.openDialog || false}
getConfirmation={taskingData.current?.getConfirmation || false}
selectCallback={taskingData.current?.selectCallback || false}
tasking_location={"process_browser"}
onTasked={() => setOpenTaskingButton(false)}/>
}
</div>
@@ -424,7 +497,7 @@ const ProcessBrowserTableTop = ({
}
return (
<Grid container spacing={0} style={{paddingTop: "10px"}}>
<Grid item xs={12}>
<Grid size={12}>
<FormControl style={{width: "30%"}}>
<InputLabel ref={inputGroupRef}>Available Groups</InputLabel>
<Select
@@ -436,7 +509,7 @@ const ProcessBrowserTableTop = ({
endAdornment={
<React.Fragment>
<MythicStyledTooltip title="View Callbacks associated with this group">
<IconButton style={{padding: "3px"}} onClick={() => {setOpenViewGroupDialog(true);}} size="large"><WidgetsIcon style={{color: theme.palette.info.main}}/></IconButton>
<IconButton style={{padding: "3px", paddingRight: "25px"}} onClick={() => {setOpenViewGroupDialog(true);}} size="large"><WidgetsIcon style={{color: theme.palette.info.main}}/></IconButton>
</MythicStyledTooltip>
</React.Fragment>
}
@@ -465,18 +538,7 @@ const ProcessBrowserTableTop = ({
<ExpandIcon color={expandOrCollapseAll ? "info" : "success"} />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={showDeletedFiles ? 'Hide Deleted Processes' : 'Show Deleted Processes'}>
<IconButton
style={{ padding: '3px' }}
onClick={onLocalToggleShowDeletedFiles}
size="large">
{showDeletedFiles ? (
<VisibilityIcon color="success" />
) : (
<VisibilityOffIcon color="error" />
)}
</IconButton>
</MythicStyledTooltip>
<div style={{paddingRight: "20px"}} />
</React.Fragment>
}
>
@@ -6,13 +6,9 @@ import {useTheme} from '@mui/material/styles';
import ClickAwayListener from '@mui/material/ClickAwayListener';
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 { 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,6 +18,14 @@ import EditIcon from '@mui/icons-material/Edit';
import AccountTreeIcon from '@mui/icons-material/AccountTree';
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,
GetMythicSetting,
useSetMythicSetting
} from "../../MythicComponents/MythicSavedUserSetting";
import {CallbacksTableColumnsReorderDialog} from "./CallbacksTableColumnsReorderDialog";
const getPermissionsDataQuery = gql`
query getPermissionsQuery($mythictree_id: Int!) {
@@ -39,22 +43,40 @@ const updateFileComment = gql`
}
}
`;
export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, me, onRowDoubleClick, onTaskRowAction, host, group, showDeletedFiles, tabInfo, expandOrCollapseAll}) => {
const columnDefaults = [
{ name: 'Info', width: 50, disableAutosize: true, disableSort: true, disableFilterMenu: true, key: "info", visible: true },
{ name: 'PID', type: 'number', key: 'process_id', inMetadata: true, width: 100, visible: true},
{ name: 'PPID', type: 'number', key: 'parent_process_id', inMetadata: true, width: 100, visible: true},
{ name: 'Name', type: 'string', disableSort: false, key: 'name_text', fillWidth: true, visible: true },
{ name: "Arch", type: 'string', key: 'architecture', inMetadata: true, width: 70, visible: true},
{ name: "Integrity", type: "number", key: "integrity_level", inMetadata: true, width: 100, visible: false },
{ name: 'Session', type: 'number', key: 'session_id', inMetadata: true, width: 100, visible: true},
{ name: "User", type: 'string', key: 'user', inMetadata: true, fillWidth: true, visible: true},
{ name: 'Tags', type: 'tags', disableSort: true, disableFilterMenu: true, width: 220, key: "tags", visible: false },
{ name: 'Comment', type: 'string', key: 'comment', disableSort: false, width: 200, visible: false },
{ name: "CMD", type: "string", key: 'command_line', inMetadata: true, fillWidth: true, visible: true},
];
const defaultVisibleColumns = ["Info","PID", "PPID", "Name", "Arch", "Session", "User", "CMD"];
export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, me, onRowDoubleClick,
onTaskRowAction, host, group, showDeletedFiles, tabInfo,
expandOrCollapseAll, getLoadedCommandForUIFeature}) => {
//const [allData, setAllData] = React.useState([]);
//console.log("treeAdjMatrix updated in table", treeAdjMatrix)
const [updateSetting] = useSetMythicSetting();
const [loading, setLoading] = React.useState(true);
const [sortData, setSortData] = React.useState({"sortKey": null, "sortDirection": null, "sortType": null});
const [openNodes, setOpenNodes] = React.useState({});
const [openContextMenu, setOpenContextMenu] = React.useState(false);
const [filterOptions, setFilterOptions] = React.useState({});
const selectedColumn = React.useRef({});
const [columnVisibility, setColumnVisibility] = React.useState({
"visible": ["Info","PID", "PPID", "Name", "User", "Arch", "CMD"],
"hidden": [ "Session", "Comment", "Tags" ]
"visible": defaultVisibleColumns,
"hidden": [ "Comment", "Tags" ]
})
const [singleTreeData, setSingleTreeData] = React.useState({});
const [viewSingleTreeData, setViewSingleTreeData] = React.useState(false);
const [openAdjustColumnsDialog, setOpenAdjustColumnsDialog] = React.useState(false);
const [openReorderDialog, setOpenReorderDialog] = React.useState(false);
const [columnOrder, setColumnOrder] = React.useState(columnDefaults);
const [updatedTreeAdjMatrix, setUpdatedTreeAdjMatrix] = React.useState(treeAdjMatrix);
const openAllNodes = (state) => {
let onodes = {};
@@ -104,9 +126,12 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
// check if key is in children anywhere, if not, add it to adjustedMatrix[host][""][key] = 1
let found = false;
for(const [keySearch, childrenSearch] of Object.entries(matrix)){
for(const [i, v] of Object.entries(childrenSearch)){
if(i === key){found=true}
if(childrenSearch.hasOwnProperty(key)){
found=true;
}
//for(const [i, v] of Object.entries(childrenSearch)){
// if(i === key){found=true}
//}
}
if(!found){
if(adjustedMatrix[group][host][""] === undefined){adjustedMatrix[group][host][""] = {}}
@@ -114,13 +139,51 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
}
}
}
// check for loops in our adjusted matrix
for(const [key, _] of Object.entries(adjustedMatrix[group][host])){
// key == 540
// does anything have 540 has a child? 760 does - 760 is visited
// does anything have 760 as a child? 676 does
// does anything have 676 as a child? 540 does - X loop detected
// let badKey = checkLoop(540, adjustedMatrix[group][host], [540]);
let removeKey = checkLoop(adjustedMatrix[group][host], [key]);
if(adjustedMatrix[group][host][removeKey]){
delete adjustedMatrix[group][host][removeKey][key];
adjustedMatrix[group][host][""][key] = 1;
}
}
}
}
//console.log("adjustedMatrix", adjustedMatrix, "realMatrix", treeAdjMatrix)
console.log("adjustedMatrix", adjustedMatrix, "realMatrix", treeAdjMatrix)
setUpdatedTreeAdjMatrix(adjustedMatrix);
}, [treeAdjMatrix]);
const checkLoop = (nodes, visited) => {
let found = false;
let checkingKey = visited[visited.length-1]; // the latest thing we've seen
for(const [testKey, testNodes] of Object.entries(nodes)){
if(testNodes.hasOwnProperty(checkingKey)){
// we found a new node that has the last node we saw as a child
found = true;
//console.log("found", testNodes, "has", checkingKey, "visited", visited, "testKey", testKey)
if(visited.includes(testKey)){
// we found a loop
//console.log("found loop", visited, testKey)
return true;
}
visited.push(testKey);
if(checkLoop(nodes, visited)){
return visited.pop()
}
}
}
if(!found){
//console.log("didn't find", checkingKey, "in any edges")
} else {
//console.log("found nested, but no loop with", checkingKey)
}
return false;
}
React.useEffect( () => {
openAllNodes(true);
setViewSingleTreeData(false);
@@ -148,21 +211,10 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
// }
};
const columnDefaults = [
{ name: 'Info', width: 50, disableAutosize: true, disableSort: true, disableFilterMenu: true },
{ name: 'PID', type: 'number', key: 'process_id', inMetadata: true, width: 100},
{ name: 'PPID', type: 'number', key: 'parent_process_id', inMetadata: true, width: 100},
{ name: 'Name', type: 'string', disableSort: false, key: 'name_text', fillWidth: true },
{ name: "User", type: 'string', key: 'user', inMetadata: true, fillWidth: true},
{ name: "Arch", type: 'string', key: 'architecture', inMetadata: true, width: 100},
{ name: 'Tags', type: 'tags', disableSort: true, disableFilterMenu: true, width: 220 },
{ name: 'Comment', type: 'string', key: 'comment', disableSort: false, width: 200 },
{ name: "CMD", type: "string", key: 'command_line', inMetadata: true, fillWidth: true},
{ name: 'Session', type: 'number', key: 'session_id', inMetadata: true, width: 100}
];
const columns = React.useMemo(
() =>
columnDefaults.reduce( (prev, cur) => {
() =>
columnOrder.reduce( (prev, cur) => {
if(columnVisibility.visible.includes(cur.name)){
if(filterOptions[cur.key] && String(filterOptions[cur.key]).length > 0){
return [...prev, {...cur, filtered: true}];
@@ -173,7 +225,7 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
return [...prev];
}
}, [])
, [filterOptions, columnVisibility]
, [filterOptions, columnVisibility, columnOrder]
);
const flattenNode = useCallback(
(node, host, group, depth = 0) => {
@@ -340,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
}
@@ -438,7 +496,7 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
}
setSingleTreeData(singleTreeAdjMatrix);
onSubmitFilterOptions({});
onSubmitFilterOptions(undefined);
}
const toggleViewSingleTreeData = () => {
setViewSingleTreeData(!viewSingleTreeData);
@@ -461,6 +519,7 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
viewSingleTreeData={viewSingleTreeData}
setSingleTree={setSingleTree}
toggleViewSingleTreeData={toggleViewSingleTreeData}
getLoadedCommandForUIFeature={getLoadedCommandForUIFeature}
onTaskRowAction={onTaskRowAction} />;
case "Name":
return <FileBrowserTableRowNameCell
@@ -505,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}
@@ -529,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)
}
})];
}
@@ -560,8 +626,12 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
};
const contextMenuOptions = [
{
name: 'Filter Column',
name: 'Filter Column', type: "item", icon: null,
click: ({event, columnIndex}) => {
if(event){
event.stopPropagation();
event.preventDefault();
}
if(columns[columnIndex].disableFilterMenu){
snackActions.warning("Can't filter that column");
return;
@@ -571,18 +641,87 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
}
},
{
name: "Show/Hide Columns",
name: "Reorder Columns and Adjust Visibility", type: "item", icon: null,
click: ({event, columnIndex}) => {
if(event){
event.stopPropagation();
event.preventDefault();
}
if(columns[columnIndex].disableFilterMenu){
snackActions.warning("Can't filter that column");
return;
}
setOpenAdjustColumnsDialog(true);
setOpenReorderDialog(true);
}
}
];
const onSubmitAdjustColumns = ({left, right}) => {
setColumnVisibility({visible: right, hidden: left});
React.useEffect( () => {
try {
const storageItem = GetMythicSetting({setting_name: `process_browser_table_columns`, default_value: defaultVisibleColumns});
if(storageItem !== null){
let allColumns = [...columnVisibility["visible"].map(c => c), ...columnVisibility["hidden"].map(c => c)];
let newHidden = [];
allColumns.forEach((v,i,a) => {
if(!storageItem.includes(v)){
newHidden.push(v);
}
});
if(storageItem.length !== 0){
setColumnVisibility({visible: storageItem, hidden: newHidden});
}
}
}catch(error){
console.log("Failed to load custom browser_table_columns", error);
}
try {
const storageItemOptions = GetMythicSetting({setting_name: `process_browser_filter_options`, default_value: {}});
if(storageItemOptions !== null){
setFilterOptions(storageItemOptions);
}
}catch(error){
console.log("Failed to load custom browser_table_filter_options", error);
}
try {
const storageColumnOrder = GetMythicSetting({setting_name: `process_browser_column_order`, default_value: columns.map(c => c.name)});
if(storageColumnOrder !== null){
let newOrder = [];
for(let i = 0; i < storageColumnOrder.length; i++){
for(let j = 0; j < columnOrder.length; j++){
if(columnOrder[j].name === storageColumnOrder[i]){
newOrder.push(columnOrder[j]);
break;
}
}
}
setColumnOrder(newOrder);
}
}catch(error){
console.log("Failed to load process_browser_table_filter_options", error);
}
setLoading(false);
}, []);
const onSubmitColumnReorder = (newOrder) => {
let newVisible = [];
let newHidden = [];
for(let i = 0; i < newOrder.length; i++){
if(newOrder[i].visible){
newVisible.push(newOrder[i].name);
} else {
newHidden.push(newOrder[i].name);
}
}
if(newVisible.length === 0){
snackActions.error("Can't update to show no fields");
return;
}
updateSetting({setting_name: `process_browser_column_order`, value: newOrder.map(c => c.name)});
setColumnOrder(newOrder);
setColumnVisibility({visible: newVisible, hidden: newHidden});
updateSetting({setting_name: `process_browser_table_columns`, value: newVisible});
setOpenReorderDialog(false);
}
const onResetColumnReorder = () => {
onSubmitColumnReorder(columnDefaults);
}
const sortColumn = columns.findIndex((column) => column.key === sortData.sortKey);
React.useEffect( () => {
@@ -595,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
@@ -602,30 +756,40 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
sortIndicatorIndex={sortColumn}
sortDirection={sortData.sortDirection}
items={gridData}
rowHeight={20}
rowHeight={GetComputedFontSize() + 7}
onClickHeader={onClickHeader}
onDoubleClickRow={localOnDoubleClick}
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>
)
}
@@ -696,9 +860,19 @@ const FileBrowserTableRowStringCell = ({cellData, treeRootData, host, rowData})
<div>{cellData}</div>
)
}
const FileBrowserTableRowActionCell = ({rowData, onTaskRowAction, treeRootData, host, viewSingleTreeData,setSingleTree, toggleViewSingleTreeData, tabInfo}) => {
const FileBrowserTableRowActionCell = ({rowData, onTaskRowAction, treeRootData, host, viewSingleTreeData,
setSingleTree, toggleViewSingleTreeData, tabInfo,
getLoadedCommandForUIFeature}) => {
const dropdownAnchorRef = React.useRef(null);
const theme = useTheme();
const loadingMenuDisplay = {
name: "Loading Dynamic Menu Items...", icon: null,
type: "item",
disabled: true,
click: ({event}) => {
event.stopPropagation();
}
};
const [dropdownOpen, setDropdownOpen] = React.useState(false);
const [viewPermissionsDialogOpen, setViewPermissionsDialogOpen] = React.useState(false);
const [permissionData, setPermissionData] = React.useState({});
@@ -721,13 +895,11 @@ const FileBrowserTableRowActionCell = ({rowData, onTaskRowAction, treeRootData,
snackActions.success('updated comment');
},
});
const [customMenuOptions, setCustomMenuOptions] = React.useState([loadingMenuDisplay]);
const onSubmitUpdatedComment = (comment) => {
updateComment({ variables: { mythictree_id: treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/].id, comment: comment } });
};
const handleDropdownToggle = (evt) => {
evt.stopPropagation();
setDropdownOpen((prevOpen) => !prevOpen);
};
const handleMenuItemClick = (event, click) => {
click({event});
setDropdownOpen(false);
@@ -737,18 +909,19 @@ const FileBrowserTableRowActionCell = ({rowData, onTaskRowAction, treeRootData,
return;
}
setDropdownOpen(false);
setCustomMenuOptions([loadingMenuDisplay]);
};
const optionsA = [
{
name: 'View Detailed Data', icon: <VisibilityIcon style={{paddingRight: "5px"}}/>,
type: "item",
type: "item", disabled: false,
click: ({event}) => {
event.stopPropagation();
getPermissions({variables: {mythictree_id: rowData.id}});
}
},
{
name: 'Edit Comment', type: "item",
name: 'Edit Comment', type: "item", disabled: false,
icon: <EditIcon style={{ paddingRight: '5px' }} />,
click: ({event}) => {
event.stopPropagation();
@@ -761,86 +934,128 @@ const FileBrowserTableRowActionCell = ({rowData, onTaskRowAction, treeRootData,
<VisibilityOffIcon style={{paddingRight: "5px", color: theme.palette.error.main}}/> :
<AccountTreeIcon style={{paddingRight: "5px", color: theme.palette.success.main}}/>,
type: "item",
disabled: false,
click: ({event}) => {
setSingleTree(rowData);
toggleViewSingleTreeData();
}
}
];
const optionsB = (callback_id, callback_display_id) => [
{
name: 'Task Inject', icon: <GetAppIcon style={{paddingRight: "5px", color: theme.palette.success.main}}/>,
type: "item",
click: ({event}) => {
event.stopPropagation();
onTaskRowAction({
process_id: treeRootData[host][rowData["full_path_text"] ].metadata.process_id,
architecture: treeRootData[host][rowData["full_path_text"] ].metadata.architecture,
uifeature: "process_browser:inject",
callback_id,
display_id: callback_display_id
});
}
},
{
name: 'Task Token Listing', icon: <ListIcon style={{paddingRight: "5px", color: theme.palette.warning.main}}/>,
type: "item",
click: ({event}) => {
event.stopPropagation();
onTaskRowAction({
process_id: treeRootData[host][rowData["full_path_text"] ].metadata.process_id,
architecture: treeRootData[host][rowData["full_path_text"] ].metadata.architecture,
uifeature: "process_browser:list_tokens",
callback_id,
display_id: callback_display_id
});
async function optionsB (callback_id, callback_display_id){
const injectCommand = await getLoadedCommandForUIFeature(callback_id, "process_browser:inject");
let injectDisplay = "Task Inject (Unsupported)";
if(injectCommand !== undefined){
injectDisplay = `Task Inject (${injectCommand.command.cmd})`
}
const tokenListCommand = await getLoadedCommandForUIFeature(callback_id, "process_browser:list_tokens");
let tokenListDisplay = "Task Token Listing (Unsupported)";
if(tokenListCommand !== undefined){
tokenListDisplay = `Task Token Listing (${tokenListCommand.command.cmd})`
}
const stealTokenCommand = await getLoadedCommandForUIFeature(callback_id, "process_browser:steal_token");
let stealTokenDisplay = "Task Steal Token (Unsupported)";
if(stealTokenCommand !== undefined){
stealTokenDisplay = `Task Steal Token (${stealTokenCommand.command.cmd})`
}
const killProcessCommand = await getLoadedCommandForUIFeature(callback_id, "process_browser:kill");
let killProcessDisplay = "Task Kill Process (Unsupported)";
if(killProcessCommand !== undefined){
killProcessDisplay = `Task Kill Process (${killProcessCommand.command.cmd})`
}
return [
{
name: injectDisplay, icon: <FontAwesomeIcon icon={faSyringe} style={{paddingRight: "5px", color: theme.palette.warning.main}}/>,
type: "item",
disabled: injectCommand === undefined,
click: ({event}) => {
event.stopPropagation();
onTaskRowAction({
process_id: treeRootData[host][rowData["full_path_text"] ].metadata.process_id,
architecture: treeRootData[host][rowData["full_path_text"] ].metadata.architecture,
uifeature: "process_browser:inject",
callback_id,
display_id: callback_display_id
});
}
},
os: ["Windows"]
},
{
name: 'Task Steal Token', icon: <DeleteIcon style={{paddingRight: "5px", color: theme.palette.error.main}}/>,
type: "item",
click: ({event}) => {
event.stopPropagation();
onTaskRowAction({
process_id: treeRootData[host][rowData["full_path_text"] ].metadata.process_id,
architecture: treeRootData[host][rowData["full_path_text"] ].metadata.architecture,
uifeature: "process_browser:steal_token",
callback_id,
display_id: callback_display_id
});
{
name: tokenListDisplay, icon: <ListIcon style={{paddingRight: "5px", color: theme.palette.warning.main}}/>,
type: "item",
disabled: tokenListCommand === undefined,
click: ({event}) => {
event.stopPropagation();
onTaskRowAction({
process_id: treeRootData[host][rowData["full_path_text"] ].metadata.process_id,
architecture: treeRootData[host][rowData["full_path_text"] ].metadata.architecture,
uifeature: "process_browser:list_tokens",
callback_id,
display_id: callback_display_id
});
},
os: ["Windows"]
},
{
name: stealTokenDisplay, icon: <FontAwesomeIcon icon={faKey} style={{paddingRight: "5px", color: theme.palette.error.main}}/>,
type: "item",
disabled: stealTokenCommand === undefined,
click: ({event}) => {
event.stopPropagation();
onTaskRowAction({
process_id: treeRootData[host][rowData["full_path_text"] ].metadata.process_id,
architecture: treeRootData[host][rowData["full_path_text"] ].metadata.architecture,
uifeature: "process_browser:steal_token",
callback_id,
display_id: callback_display_id
});
},
os: ["Windows"]},
{
name: killProcessDisplay, icon: <FontAwesomeIcon icon={faSkullCrossbones} style={{paddingRight: "5px", color: theme.palette.error.main}}/>,
type: "item",
disabled: killProcessCommand === undefined,
click: ({event}) => {
event.stopPropagation();
onTaskRowAction({
process_id: treeRootData[host][rowData["full_path_text"] ].metadata.process_id,
architecture: treeRootData[host][rowData["full_path_text"] ].metadata.architecture,
uifeature: "process_browser:kill",
confirm_dialog: true,
callback_id,
display_id: callback_display_id
});
}
},
os: ["Windows"]},
{
name: 'Task Kill Process', icon: <DeleteIcon style={{paddingRight: "5px", color: theme.palette.error.main}}/>,
type: "item",
click: ({event}) => {
event.stopPropagation();
onTaskRowAction({
process_id: treeRootData[host][rowData["full_path_text"] ].metadata.process_id,
architecture: treeRootData[host][rowData["full_path_text"] ].metadata.architecture,
uifeature: "process_browser:kill",
confirm_dialog: true,
callback_id,
display_id: callback_display_id
});
}
},
];
const getMenuOptions = () => {
];
}
async function getMenuOptions() {
let options = [...optionsA];
options.push(...optionsB(tabInfo["callbackID"], tabInfo["displayID"]));
options.push(...(await optionsB(tabInfo["callbackID"], tabInfo["displayID"])));
if(treeRootData[host][rowData["full_path_text"] ]?.callback?.["id"] !== tabInfo["callbackID"]){
options.push({
name: "Original Callback", icon: null, click: () => {}, type: "menu",
name: `Original Callback: ${treeRootData[host][rowData["full_path_text"] ]?.callback?.["id"]}`,
icon: null, click: () => {}, type: "menu",
menuItems: [
...optionsB(rowData?.callback?.id, rowData?.callback?.display_id)
...(await optionsB(treeRootData[host][rowData["full_path_text"] ]?.callback?.["id"],
treeRootData[host][rowData["full_path_text"] ]?.callback?.display_id))
]
})
}
return options;
}
const handleDropdownToggle = (evt) => {
evt.stopPropagation();
setDropdownOpen((prevOpen) => !prevOpen);
if(!dropdownOpen){
getMenuOptions().then(r => {
if(r){
setCustomMenuOptions(r);
}
});
} else {
setCustomMenuOptions([loadingMenuDisplay]);
}
};
return (
treeRootData[host][rowData["full_path_text"] ]?.id ? (
<React.Fragment>
@@ -863,12 +1078,13 @@ const FileBrowserTableRowActionCell = ({rowData, onTaskRowAction, treeRootData,
isOpen={dropdownAnchorRef.current}
onOpen={setDropdownOpen}
externallyOpen={dropdownOpen}
anchorReference={"anchorEl"}
menu={[
...getMenuOptions().map((option, index) => (
...customMenuOptions.map((option, index) => (
option.type === 'item' ? (
<DropdownMenuItem
key={option.name}
disabled={!(option.os === undefined || option.os.includes(treeRootData[host][rowData["full_path_text"]].os))}
disabled={option.disabled}
onClick={(event) => handleMenuItemClick(event, option.click)}
>
{option.icon} {option.name}
@@ -881,7 +1097,7 @@ const FileBrowserTableRowActionCell = ({rowData, onTaskRowAction, treeRootData,
option.menuItems.map((menuOption, indx) => (
<DropdownMenuItem
key={menuOption.name}
disabled={!(menuOption.os === undefined || menuOption.os.includes(treeRootData[host][rowData["full_path_text"]].os))}
disabled={menuOption.disabled}
onClick={(event) => handleMenuItemClick(event, menuOption.click)}
>
{menuOption.icon}{menuOption.name}
@@ -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`
@@ -64,11 +65,11 @@ const columns = [
width: 100,
renderCell: (params) => <CallbacksTableIPCell rowData={params.row} cellData={params.row.ip} />,
sortable: false,
valueGetter: (params) => {
valueGetter: (value, row) => {
try{
return JSON.parse(params.row.ip)[0];
return JSON.parse(row.ip)[0];
}catch(error){
return params.row.ip;
return row.ip;
}
}
},
@@ -76,7 +77,7 @@ const columns = [
field: "last_checkin",
headerName: "Checkin",
width: 100,
valueGetter: (params) => new Date(params.row.last_checkin),
valueGetter: (value, row) => new Date(row.last_checkin),
renderCell: (params) =>
<CallbacksTableLastCheckinCell rowData={params.row} />,
},
@@ -88,10 +89,13 @@ const columns = [
];
const CustomSelectTable = ({initialData, selectedData}) => {
const [data, setData] = React.useState([]);
const [rowSelectionModel, setRowSelectionModel] = React.useState([]);
const [rowSelectionModel, setRowSelectionModel] = React.useState({
type: 'include',
ids: new Set([]),
});
React.useEffect( () => {
selectedData.current = data.reduce( (prev, cur) => {
if(rowSelectionModel.includes(cur.id)){return [...prev, cur]}
if(rowSelectionModel.ids.has(cur.id)){return [...prev, cur]}
return [...prev];
}, []);
}, [data, rowSelectionModel]);
@@ -286,33 +290,37 @@ export function CallbacksTabsTaskMultipleDialog({onClose, callback}) {
}
}
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Task Multiple {callback.payload.payloadtype.name} Callbacks at Once</DialogTitle>
<CustomSelectTable initialData={initialData}
selectedData={selectedData} />
<Grid item xs={12} >
<CallbacksTabsTaskingInput filterTasks={false} onSubmitFilter={()=>{}} onSubmitCommandLine={onSubmitCommandLine}
changeSelectedToken={changeSelectedToken}
payloadtype_name={callback.payload.payloadtype.name}
filterOptions={{}} callback_id={callback.id} callback_os={callback.payload.os} parentMountedRef={mountedRef} />
</Grid>
{openTaskingButton &&
<TaskFromUIButton cmd={taskingData.current?.cmd}
callback_id={taskingData?.current?.callback_id || 0}
callback_ids={taskingData?.current?.callback_ids || undefined}
parameters={taskingData.current?.parameters || ""}
openDialog={taskingData.current?.openDialog || false}
tasking_location={taskingData.current?.tasking_location || "command_line"}
dontShowSuccessDialog={taskingData.current?.dontShowSuccessDialog || false}
selectCallback={taskingData.current?.selectCallback || false}
onTasked={onTasked}/>
}
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
Cancel
</Button>
</DialogActions>
</React.Fragment>
<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 &&
<TaskFromUIButton cmd={taskingData.current?.cmd}
callback_id={taskingData?.current?.callback_id || 0}
callback_ids={taskingData?.current?.callback_ids || undefined}
parameters={taskingData.current?.parameters || ""}
openDialog={taskingData.current?.openDialog || false}
tasking_location={taskingData.current?.tasking_location || "command_line"}
dontShowSuccessDialog={taskingData.current?.dontShowSuccessDialog || false}
selectCallback={taskingData.current?.selectCallback || false}
onTasked={onTasked}/>
}
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
Cancel
</Button>
</DialogActions>
</React.Fragment>
);
}
@@ -13,6 +13,7 @@ import {MythicModifyStringDialog} from '../../MythicComponents/MythicDialog';
import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip';
import {taskingDataFragment, createTaskingMutation} from "./CallbackMutations";
import { validate as uuidValidate } from 'uuid';
import {getSkewedNow} from "../../utilities/Time";
export function CallbacksTabsTaskingLabel(props){
@@ -46,8 +47,8 @@ export function CallbacksTabsTaskingLabel(props){
},
]);
return (
<React.Fragment>
<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={
@@ -55,7 +56,7 @@ export function CallbacksTabsTaskingLabel(props){
}
/>
}
</React.Fragment>
</>
)
}
@@ -81,14 +82,14 @@ query getBatchTasking($callback_id: Int!, $offset: Int!, $fetchLimit: Int!){
}
}
`;
export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, parentMountedRef, me}) =>{
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);
const [commandInfo, setCommandInfo] = React.useState({});
const [taskingData, setTaskingData] = React.useState({task: []});
const taskingDataRef = React.useRef({task: []});
const [fromNow, setFromNow] = React.useState((new Date()).toISOString());
const [fromNow, setFromNow] = React.useState(getSkewedNow().toISOString());
const [selectedToken, setSelectedToken] = React.useState({});
const [filterOptions, setFilterOptions] = React.useState({
"operatorsList": [],
@@ -174,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) => {
@@ -269,7 +273,7 @@ export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, pa
getInfiniteScrollTasking({variables: {callback_id: tabInfo.callbackID, offset: taskingData.task.length, fetchLimit}});
}
const onSubmitCommandLine = (message, cmd, parsed, force_parsed_popup, cmdGroupNames, previousTaskingLocation) => {
//console.log(message, cmd, parsed);
console.log(message, cmd, parsed);
let params = message.split(" ");
delete params[0];
params = params.join(" ").trim();
@@ -370,14 +374,19 @@ export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, pa
setSelectedToken(token);
}
}
const [collapseAllRequest, setCollapseAllRequest] = React.useState(0);
React.useEffect( () => {
if(collapseTaskRequest[index] !== undefined){
setCollapseAllRequest(collapseAllRequest + 1);
}
}, [collapseTaskRequest]);
return (
<MythicTabPanel index={index} value={value} >
{!fetched && <LinearProgress color="primary" thickness={2} style={{paddingTop: "5px"}}/>}
{loadingMore && <LinearProgress color="primary" thickness={2} style={{paddingTop: "5px"}}/>}
<div style={{overflowY: "auto", flexGrow: 1, width: "100%"}} id={`taskingPanel${tabInfo.callbackID}`}>
{!fetchedAllTasks &&
<MythicStyledTooltip style={{marginLeft: "50%"}} title="Fetch Older Tasks">
<MythicStyledTooltip tooltipStyle={{marginLeft: "50%"}} title="Fetch Older Tasks">
<IconButton
onClick={loadMoreTasks}
variant="contained"
@@ -388,6 +397,7 @@ export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, pa
taskingData.task.map((task) => (
<TaskDisplay key={"taskinteractdisplay" + task.id} me={me} task={task}
command_id={task.command == null ? 0 : task.command.id}
collapseAllRequest={collapseAllRequest}
filterOptions={filterOptions} newlyIssuedTasks={newlyIssuedTasks.current}/>
))
}
@@ -399,6 +409,7 @@ export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, pa
onSubmitCommandLine={onSubmitCommandLine}
changeSelectedToken={changeSelectedToken}
filterOptions={filterOptions}
focus={index === value}
callback_id={tabInfo.callbackID}
payloadtype_name={tabInfo.payloadtype}
callback_os={tabInfo.os} parentMountedRef={mountedRef} />
@@ -13,6 +13,7 @@ import {MythicModifyStringDialog} from '../../MythicComponents/MythicDialog';
import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip';
import {taskingDataFragment, createTaskingMutation} from "./CallbackMutations";
import { validate as uuidValidate } from 'uuid';
import {getSkewedNow} from "../../utilities/Time";
export function CallbacksTabsTaskingConsoleLabel(props){
@@ -47,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={
@@ -81,14 +82,14 @@ 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);
const [commandInfo, setCommandInfo] = React.useState({});
const [taskingData, setTaskingData] = React.useState({task: []});
const taskingDataRef = React.useRef({task: []});
const [fromNow, setFromNow] = React.useState((new Date()).toISOString());
const [fromNow, setFromNow] = React.useState(getSkewedNow().toISOString());
const [selectedToken, setSelectedToken] = React.useState({});
const [filterOptions, setFilterOptions] = React.useState({
"operatorsList": [],
@@ -174,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) => {
@@ -398,7 +402,7 @@ export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onClose
<CallbacksTabsTaskingInput filterTasks={true} me={me} onSubmitFilter={onSubmitFilter} onSubmitCommandLine={onSubmitCommandLine} changeSelectedToken={changeSelectedToken}
filterOptions={filterOptions} callback_id={tabInfo.callbackID}
payloadtype_name={tabInfo.payloadtype}
payloadtype_name={tabInfo.payloadtype} focus={index === value}
callback_os={tabInfo.os} parentMountedRef={mountedRef} />
{openParametersDialog &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openParametersDialog}
@@ -8,7 +8,7 @@ import {CallbacksTabsTaskingFilterDialog} from './CallbacksTabsTaskingFilterDial
import {CallbacksTabsTaskingInputTokenSelect} from './CallbacksTabsTaskingInputTokenSelect';
import { gql, useSubscription, useMutation } from '@apollo/client';
import { snackActions } from '../../utilities/Snackbar';
import { meState } from '../../../cache';
import {meState, operatorSettingDefaults} from '../../../cache';
import {useReactiveVar} from '@apollo/client';
import { validate as uuidValidate } from 'uuid';
import {MythicSelectFromListDialog} from "../../MythicComponents/MythicSelectFromListDialog";
@@ -16,7 +16,10 @@ import { Backdrop } from '@mui/material';
import {CircularProgress} from '@mui/material';
import {getDynamicQueryParams} from "./TaskParametersDialogRow";
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
import {GetMythicSetting, SetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
import {GetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
import {getSkewedNow} from "../../utilities/Time";
import { useTheme } from '@mui/material/styles';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
const GetLoadedCommandsSubscription = gql`
subscription GetLoadedCommandsSubscription($callback_id: Int!){
@@ -88,7 +91,23 @@ subscription tasksSubscription($callback_id: Int!){
}
}
`;
const contextSubscription = gql`
subscription CallbackMetadataForTasking($callback_id: Int!){
callback_stream(batch_size: 1, cursor: {initial_value: {timestamp: "1970-01-01"}}, where: {id: {_eq: $callback_id} }){
cwd
impersonation_context
extra_info
host
pid
process_short_name
user
ip
color
integrity_level
architecture
}
}
`;
const GetUpDownArrowName = (task, useDisplayParamsForCLIHistoryUserSetting) => {
if(task.command){
if(task?.command?.payloadtype?.use_display_params_for_cli_history){
@@ -134,7 +153,6 @@ const IsCLIPossibleParameterType = (parameter_type) => {
case "TypedArray":
case "ChooseMultiple":
case "String":
case "File":
return true;
default:
return false;
@@ -151,11 +169,27 @@ const IsRepeatableCLIParameterType = (parameter_type) => {
return false;
}
}
export function CallbacksTabsTaskingInputPreMemo(props){
const toastId = "tasking-toast-message";
const snackMessageStyles = {position:"bottom-left", autoClose: 1000, style:{marginBottom: "50px"}, toastId: toastId};
const snackReverseSearchMessageStyles = {position:"bottom-left", autoClose: 1000, style:{marginBottom: "100px"}, toastId: toastId};
const theme = useTheme();
const inputRef = React.useRef(null);
const snackMessageStyles = {position:"bottom-left", autoClose: 1000, toastId: toastId, style: {marginBottom: "30px"}};
const snackReverseSearchMessageStyles = {position:"bottom-left", autoClose: 1000, toastId: toastId, style: {marginBottom: "70px"}};
const [commandPayloadType, setCommandPayloadType] = React.useState("");
const [callbackContext, setCallbackContext] = React.useState({
cwd: "",
impersonation_context: "",
extra_info: "",
ip: "",
host: "",
user: "",
pid: "",
process_short_name: "",
color: "",
integrity_level: 2,
architecture: "",
});
const [message, setMessage] = React.useState("");
const loadedOptions = React.useRef([]);
const taskOptions = React.useRef([]);
@@ -170,7 +204,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
onCompleted: (data) => {
if(data.dynamic_query_function.status === "success"){
try{
if(data.dynamic_query_function.choices.length > 0){
if(data.dynamic_query_function.choices && data.dynamic_query_function.choices.length > 0){
const choices = data.dynamic_query_function.choices.filter( c => {
if(c.toLowerCase().includes(lastValueTypedBeforeDynamicParamsRef.current.toLowerCase())){
return c;
@@ -197,6 +231,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
snackActions.warning("no available options", snackMessageStyles);
}
}catch(error){
console.log(error);
setBackdropOpen(false);
snackActions.warning("Failed to parse dynamic parameter results", snackMessageStyles);
tabOptions.current = [];
@@ -227,7 +262,10 @@ export function CallbacksTabsTaskingInputPreMemo(props){
const commandOptionsForcePopup = React.useRef(false);
const [openSelectCommandDialog, setOpenSelectCommandDialog] = React.useState(false);
const me = useReactiveVar(meState);
const useDisplayParamsForCLIHistoryUserSetting = React.useRef(GetMythicSetting({setting_name: "useDisplayParamsForCLIHistory", default_value: "true"}));
const useDisplayParamsForCLIHistoryUserSetting = React.useRef(GetMythicSetting({setting_name: "useDisplayParamsForCLIHistory", default_value: operatorSettingDefaults.useDisplayParamsForCLIHistory}));
const hideTaskingContext = React.useRef(GetMythicSetting({setting_name: "hideTaskingContext", default_value: operatorSettingDefaults.hideTaskingContext}));
hideTaskingContext.current = props.hide_context | hideTaskingContext.current;
const taskingContextFields = React.useRef(GetMythicSetting({setting_name: "taskingContextFields", default_value: operatorSettingDefaults.taskingContextFields}));
const forwardOrBackwardTabIndex = (event, currentIndex, options) => {
if(event.shiftKey){
let newIndex = currentIndex - 1;
@@ -247,8 +285,28 @@ export function CallbacksTabsTaskingInputPreMemo(props){
return;
}
tokenOptions.current = data.data.callbacktoken;
if(tokenOptions.current.length === 0) {
props.changeSelectedToken("Default Token");
}
}
});
useSubscription(contextSubscription, {
variables: {callback_id: props.callback_id}, fetchPolicy: "network-only",
shouldResubscribe: true,
onData: ({data}) => {
if(!mountedRef.current || !props.parentMountedRef.current){
return;
}
let newContext = data.data.callback_stream[0];
try{
let ips = JSON.parse(newContext.ip);
newContext.ip = ips[0];
}catch(error){
newContext.ip = "";
}
setCallbackContext(newContext);
}
});
useSubscription(subscriptionTask, {
variables: {callback_id: props.callback_id}, fetchPolicy: "network-only",
shouldResubscribe: true,
@@ -454,7 +512,20 @@ export function CallbacksTabsTaskingInputPreMemo(props){
console.log("lastSuppliedParameter", lastSuppliedParameter, "has_value", lastSuppliedParameterHasValue);
lastValueTypedBeforeDynamicParamsRef.current = lastSuppliedParameterHasValue;
if (lastSuppliedParameter !== undefined && parsed[lastSuppliedParameter.cli_name] !== undefined && lastSuppliedParameterHasValue === ""){
if(lastSuppliedParameter.dynamic_query_function !== ""){
if (lastSuppliedParameter.choices.length > 0){
const choices = lastSuppliedParameter.choices.filter( c => {
if(c.toLowerCase().includes(lastValueTypedBeforeDynamicParamsRef.current.toLowerCase())){
return c;
}
})
tabOptions.current = choices;
tabOptionsIndex.current = 0;
tabOptionsType.current = "param_value";
let newChoice = choices[0].includes(" ") ? "\"" + choices[0] + "\"" : choices[0];
let newMsg = message.substring(0, message.length - lastValueTypedBeforeDynamicParamsRef.current.length) + newChoice;
setMessage(newMsg);
return;
} else if(lastSuppliedParameter.dynamic_query_function !== ""){
setBackdropOpen(true);
//snackActions.info("Querying payload type container for options...", snackMessageStyles);
getDynamicParams({variables:{
@@ -464,14 +535,6 @@ export function CallbacksTabsTaskingInputPreMemo(props){
payload_type: cmd.payloadtype.name
}});
return;
} else if (lastSuppliedParameter.choices.length > 0){
tabOptions.current = lastSuppliedParameter.choices;
tabOptionsIndex.current = 0;
tabOptionsType.current = "param_value";
let newChoice = lastSuppliedParameter.choices[0].includes(" ") ? "\"" + lastSuppliedParameter.choices[0] + "\"" : lastSuppliedParameter.choices[0];
let newMsg = message + newChoice;
setMessage(newMsg);
return;
}
}
console.log("cmdGroupNames in tab", cmdGroupNames);
@@ -554,7 +617,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
// what the user typed isn't an exact match, so find things that start with what they're trying to type
paramOptions = cmd.commandparameters.reduce( (prev, cur) => {
if(cmdGroupNames.includes(cur.parameter_group_name) &&
cur.cli_name.toLowerCase().startsWith(lastFlag.slice(1).toLocaleLowerCase()) &&
cur.cli_name.toLowerCase().includes(lastFlag.slice(1).toLocaleLowerCase()) &&
IsCLIPossibleParameterType(cur.parameter_type) &&
(!(cur.cli_name in parsed) || (IsRepeatableCLIParameterType(cur.parameter_type)) ) ){
return [...prev, cur.cli_name];
@@ -604,7 +667,20 @@ export function CallbacksTabsTaskingInputPreMemo(props){
console.log("lastSuppliedParameter", lastSuppliedParameter)
lastValueTypedBeforeDynamicParamsRef.current = lastSuppliedParameterHasValue;
if (lastSuppliedParameter !== undefined && parsed[lastSuppliedParameter.cli_name] !== undefined){
if(lastSuppliedParameter.dynamic_query_function !== ""){
if (lastSuppliedParameter.choices.length > 0){
const choices = lastSuppliedParameter.choices.filter( c => {
if(c.toLowerCase().includes(lastValueTypedBeforeDynamicParamsRef.current.toLowerCase())){
return c;
}
})
tabOptions.current = choices;
tabOptionsIndex.current = 0;
tabOptionsType.current = "param_value";
let newChoice = choices[0].includes(" ") ? "\"" + choices[0] + "\"" : choices[0];
let newMsg = message.substring(0, message.length - lastValueTypedBeforeDynamicParamsRef.current.length) + newChoice;
setMessage(newMsg);
return;
} else if(lastSuppliedParameter.dynamic_query_function !== ""){
setBackdropOpen(true);
//snackActions.info("Querying payload type container for options...", snackMessageStyles);
getDynamicParams({variables:{
@@ -614,14 +690,6 @@ export function CallbacksTabsTaskingInputPreMemo(props){
payload_type: cmd.payloadtype.name
}});
return;
} else if (lastSuppliedParameter.choices.length > 0){
tabOptions.current = lastSuppliedParameter.choices;
tabOptionsIndex.current = 0;
tabOptionsType.current = "param_value";
let newChoice = lastSuppliedParameter.choices[0].includes(" ") ? "\"" + lastSuppliedParameter.choices[0] + "\"" : lastSuppliedParameter.choices[0];
let newMsg = message + newChoice;
setMessage(newMsg);
return;
}
}
return;
@@ -637,10 +705,20 @@ export function CallbacksTabsTaskingInputPreMemo(props){
}else{
// somebody hit tab with either a blank message or a partial word
if(tabOptions.current.length === 0){
let opts = loadedOptions.current.filter( l => l.cmd.toLowerCase().startsWith(message.toLocaleLowerCase()) && (l.attributes.supported_os.length === 0 || l.attributes.supported_os.includes(props.callback_os)));
tabOptions.current = opts;
let opts = loadedOptions.current.filter( l => l.cmd.toLowerCase().includes(message.toLocaleLowerCase()) && (l.attributes.supported_os.length === 0 || l.attributes.supported_os.includes(props.callback_os)));
tabOptionsType.current = "param_name";
tabOptionsIndex.current = 0;
let startsWithOpts = opts.filter(s => s.cmd.startsWith(message.toLocaleLowerCase()));
let includesOpts = opts.filter(s => {
for(let i = 0; i < startsWithOpts.length; i++){
if(startsWithOpts[i].cmd === s.cmd){
return false;
}
}
return true;
})
opts = [...startsWithOpts, ...includesOpts];
tabOptions.current = opts;
if(opts.length > 0){
setMessage(opts[0].cmd);
setCommandPayloadType(opts[0]?.payloadtype?.name || "");
@@ -698,7 +776,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
taskOptionsIndex.current = -1;
}
}
if(message === ""){
if(message === "" && tabOptions.current.length === 0){
setCommandPayloadType("");
}
}
@@ -715,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;
}
@@ -749,11 +827,12 @@ export function CallbacksTabsTaskingInputPreMemo(props){
buffer += value;
return;
}
else if(value === " "){
else if([" ", "\t", "\r", "\n"].includes(value)){
if(backslash){
backslash = false;
buffer += " ";
return;
//buffer += " ";
buffer += "\\";
//return;
}
if(buffer.length > 0){
if(buffer[buffer.length-1] === buffer[0] && [`'`, `"`].includes(buffer[0])){
@@ -1052,7 +1131,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
if(`-${cmd.commandparameters[j].cli_name}` === argv[i]){
last_command_parameter = cmd.commandparameters[j];
has_value = i !== argv.length -1;
break;
return [last_command_parameter, has_value ? argv[argv.length-1] : ""];
}
}
}
@@ -1333,23 +1412,25 @@ export function CallbacksTabsTaskingInputPreMemo(props){
let cmdGroupName = ["Default"];
let parsedWithPositionalParameters = {};
let params = splitMessage.slice(1).join(" ");
if(unmodifiedHistoryValue.includes("modal") || unmodifiedHistoryValue.includes("browserscript")){
// these are the two kinds that'll introduce dictionary values as original_params
try{
parsedWithPositionalParameters = JSON.parse(params);
cmdGroupName = determineCommandGroupName(cmd, parsedWithPositionalParameters);
if(cmdGroupName !== undefined){
cmdGroupName.sort()
} else {
snackActions.warning("Two or more of the specified parameters can't be used together", snackMessageStyles);
return;
}
}catch(error){
snackActions.warning("Failed to parse modified JSON value", snackMessageStyles);
let failed_json_parse = true;
try{
parsedWithPositionalParameters = JSON.parse(params);
if(['string', 'number', 'boolean', null].includes(typeof parsedWithPositionalParameters)){
throw("failed to parse json");
}
cmdGroupName = determineCommandGroupName(cmd, parsedWithPositionalParameters);
if(cmdGroupName !== undefined){
cmdGroupName.sort()
} else {
snackActions.warning("Two or more of the specified parameters can't be used together", snackMessageStyles);
return;
}
}else{
failed_json_parse = false;
}catch(error){
failed_json_parse = true;
}
if(failed_json_parse){
let parsed = parseCommandLine(params, cmd);
//console.log("result of parseCommandLine", parsed, !parsed)
if(parsed === undefined){
@@ -1368,7 +1449,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
if(cmd.commandparameters.length > 0){
parsed["_"].unshift(cmd);
parsedWithPositionalParameters = fillOutPositionalArguments(cmd, parsed, cmdGroupName);
//console.log(parsedWithPositionalParameters);
console.log("what's left", parsedWithPositionalParameters);
if(parsedWithPositionalParameters === undefined){
return;
}
@@ -1392,14 +1473,14 @@ export function CallbacksTabsTaskingInputPreMemo(props){
}else{
let simplifiedGroupName = simplifyGroupNameChoices(cmdGroupName, cmd, parsedWithPositionalParameters)
if(simplifiedGroupName === "" ){
snackActions.warning("Passed arguments are ambiguous, use shift+enter for modal or provide more parameters", snackMessageStyles);
return;
props.onSubmitCommandLine(message, cmd, parsedWithPositionalParameters, true, cmdGroupName, unmodifiedHistoryValue)
} else {
props.onSubmitCommandLine(message, cmd, parsedWithPositionalParameters, Boolean(commandOptionsForcePopup.current), [simplifiedGroupName], unmodifiedHistoryValue);
}
}
}
setMessage("");
setCommandPayloadType("");
taskOptionsIndex.current = -1;
reverseSearchIndex.current = -1;
setReverseSearching(false);
@@ -1408,8 +1489,10 @@ export function CallbacksTabsTaskingInputPreMemo(props){
}
console.log("positional args added in:", parsedWithPositionalParameters);
console.log("about to call onSubmitCommandLine", cmd);
console.log("commandOptionsForcePopup", Boolean(commandOptionsForcePopup.current), "group name", cmdGroupName)
props.onSubmitCommandLine(message, cmd, parsedWithPositionalParameters, Boolean(commandOptionsForcePopup.current), cmdGroupName, unmodifiedHistoryValue);
setMessage("");
setCommandPayloadType("");
taskOptionsIndex.current = -1;
reverseSearchIndex.current = -1;
setReverseSearching(false);
@@ -1451,13 +1534,14 @@ export function CallbacksTabsTaskingInputPreMemo(props){
setReverseSearchString(event.target.value);
if(event.target.value.length === 0){
setMessage("");
setCommandPayloadType("");
reverseSearchOptions.current = [];
reverseSearchIndex.current = 0;
return;
}
// need to do a reverse i search through taskOptions
const lowerCaseTextSearch = event.target.value.toLowerCase();
const matchingOptions = taskOptions.current.filter( x => (GetCommandName(x)).toLowerCase().includes(lowerCaseTextSearch));
const matchingOptions = taskOptions.current.filter( x => (GetCommandName(x) + x.original_params).toLowerCase().includes(lowerCaseTextSearch));
const filteredMatches = matchingOptions.filter( x => applyFilteringToTasks(x))
reverseSearchOptions.current = filteredMatches;
if(filteredMatches.length > 0){
@@ -1518,32 +1602,130 @@ export function CallbacksTabsTaskingInputPreMemo(props){
event.preventDefault();
}
}
React.useEffect( () => {
if(inputRef.current){
inputRef.current.focus();
}
}, [props.focus])
return (
<React.Fragment>
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={false}>
<CircularProgress color="inherit" />
<div style={{position: "relative"}}>
{backdropOpen && <Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={false}>
<CircularProgress color="inherit" size={30}/>
</Backdrop>
}
{reverseSearching &&
<TextField
placeholder={"Search previous commands"}
onKeyDown={onReverseSearchKeyDown}
onChange={handleReverseSearchInputChange}
onKeyDown={onReverseSearchKeyDown}
onChange={handleReverseSearchInputChange}
size="small"
color={"secondary"}
autoFocus={true}
variant="outlined"
value={reverseSearchString}
fullWidth={true}
InputProps={{ type: 'search',
startAdornment: <React.Fragment><Typography style={{width: "10%"}}>reverse-i-search:</Typography></React.Fragment>
InputProps={{
type: 'search',
startAdornment: <React.Fragment><Typography
style={{width: "10%"}}>reverse-i-search:</Typography></React.Fragment>
}}
/>
}
{callbackContext?.impersonation_context !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("impersonation_context") &&
<MythicStyledTooltip title={"Impersonation Context"}>
<span className={"rounded-tab"} style={{
backgroundColor: theme.taskContextImpersonationColor,
borderColor: callbackContext.color === "" ? "" : callbackContext.color
}}>
<b>{"User: "}</b>{callbackContext.impersonation_context}
</span>
</MythicStyledTooltip>
}
{callbackContext?.user !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("user") &&
<MythicStyledTooltip title={"User Context" + (callbackContext.integrity_level > 2 ? " (high integrity)" : "")}>
<span className={"rounded-tab"} style={{
backgroundColor: theme.taskContextColor,
borderColor: callbackContext.color === "" ? "" : callbackContext.color
}}>
<b>{"User: "}</b>{callbackContext.user}{callbackContext.integrity_level > 2 ? "*" : ""}
</span>
</MythicStyledTooltip>
}
{callbackContext?.cwd !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("cwd") &&
<MythicStyledTooltip title={"Current Working Directory"}>
<span className={"rounded-tab"} style={{
backgroundColor: theme.taskContextColor,
borderColor: callbackContext.color === "" ? "" : callbackContext.color
}}>
<b>{"Dir: "}</b>{callbackContext.cwd}
</span>
</MythicStyledTooltip>
}
{callbackContext?.host !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("host") &&
<MythicStyledTooltip title={"Hostname"}>
<span className={"rounded-tab"} style={{
backgroundColor: theme.taskContextColor,
borderColor: callbackContext.color === "" ? "" : callbackContext.color
}}>
<b>{"Host: "}</b>{callbackContext.host}
</span>
</MythicStyledTooltip>
}
{callbackContext?.ip !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("ip") &&
<MythicStyledTooltip title={"First IP Address"}>
<span className={"rounded-tab"} style={{
backgroundColor: theme.taskContextColor,
borderColor: callbackContext.color === "" ? "" : callbackContext.color
}}>
<b>{"IP: "}</b>{callbackContext.ip}
</span>
</MythicStyledTooltip>
}
{callbackContext?.pid !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("pid") &&
<MythicStyledTooltip title={"Process ID"}>
<span className={"rounded-tab"} style={{
backgroundColor: theme.taskContextColor,
borderColor: callbackContext.color === "" ? "" : callbackContext.color
}}>
<b>{"PID: "}</b>{callbackContext.pid}
</span>
</MythicStyledTooltip>
}
{callbackContext?.architecture !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("architecture") &&
<MythicStyledTooltip title={"Process Architecture"}>
<span className={"rounded-tab"} style={{
backgroundColor: theme.taskContextColor,
borderColor: callbackContext.color === "" ? "" : callbackContext.color
}}>
<b>{"Arch: "}</b>{callbackContext.architecture}
</span>
</MythicStyledTooltip>
}
{callbackContext?.process_short_name !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("process_short_name") &&
<MythicStyledTooltip title={"Process Name"}>
<span className={"rounded-tab"} style={{
backgroundColor: theme.taskContextColor,
borderColor: callbackContext.color === "" ? "" : callbackContext.color
}}>
<b>{"Process: "}</b>{callbackContext.process_short_name}
</span>
</MythicStyledTooltip>
}
{callbackContext?.extra_info !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("extra_info") &&
<MythicStyledTooltip title={"Extra Callback Context"}>
<span className={"rounded-tab"} style={{
backgroundColor: theme.taskContextExtraColor,
borderColor: callbackContext.color === "" ? "" : callbackContext.color
}}>
{callbackContext.extra_info}
</span>
</MythicStyledTooltip>
}
<TextField
placeholder={"Task an agent..."}
onKeyDown={onKeyDown}
onChange={handleInputChange}
onKeyDown={onKeyDown}
onChange={handleInputChange}
size="small"
color={"secondary"}
variant="outlined"
@@ -1553,56 +1735,78 @@ export function CallbacksTabsTaskingInputPreMemo(props){
value={message}
autoFocus={true}
fullWidth={true}
inputRef={inputRef}
style={{marginBottom: "0px", marginTop: "0px", paddingTop: "0px"}}
InputProps={{
type: 'search',
spellCheck: false,
autoFocus: true,
style: {paddingTop: "0px", paddingBottom: "0px", paddingRight: "5px"},
endAdornment:
<React.Fragment>
<IconButton
color="info"
variant="contained"
onClick={onSubmitCommandLine}
size="large"><SendIcon/></IconButton>
{props.filterTasks &&
<IconButton
color={activeFiltering ? "warning" : "secondary"}
variant="contained"
onClick={onClickFilter}
style={{paddingLeft: 0}}
size="large"><TuneIcon/></IconButton>
}
{commandPayloadType !== "" &&
<MythicAgentSVGIcon payload_type={commandPayloadType}
style={{width: "35px", height: "35px"}}/>
}
</React.Fragment>
<React.Fragment>
<IconButton
color="info"
variant="contained"
disableRipple={true}
disableFocusRipple={true}
onClick={onSubmitCommandLine}
size="large"><SendIcon/>
</IconButton>
{props.filterTasks &&
<IconButton
color={activeFiltering ? "warning" : "secondary"}
variant="contained"
onClick={onClickFilter}
style={{paddingLeft: 0}}
disableRipple={true}
disableFocusRipple={true}
size="large"><TuneIcon/></IconButton>
}
{commandPayloadType !== "" &&
<MythicAgentSVGIcon payload_type={commandPayloadType}
style={{width: "35px", height: "35px"}}/>
}
</React.Fragment>
,
startAdornment: <React.Fragment>
{tokenOptions.current.length > 0 ? (
<CallbacksTabsTaskingInputTokenSelect options={tokenOptions} changeSelectedToken={props.changeSelectedToken}/>
<CallbacksTabsTaskingInputTokenSelect options={tokenOptions.current}
changeSelectedToken={props.changeSelectedToken}/>
) : null}
</React.Fragment>
}}
/>
{openFilterOptionsDialog &&
<MythicDialog fullWidth={true} maxWidth="md" open={openFilterOptionsDialog}
onClose={()=>{setOpenFilterOptionsDialog(false);}}
innerDialog={<CallbacksTabsTaskingFilterDialog filterCommandOptions={loadedOptions} onSubmit={props.onSubmitFilter} filterOptions={props.filterOptions} onClose={()=>{setOpenFilterOptionsDialog(false);}} />}
/>
}
{openSelectCommandDialog &&
<MythicDialog fullWidth={true} maxWidth="md" open={openSelectCommandDialog}
onClose={()=>{setOpenSelectCommandDialog(false)}}
innerDialog={<MythicSelectFromListDialog onClose={()=>{setOpenSelectCommandDialog(false);}}
onSubmit={processCommandAndCommandLine} options={commandOptions.current} title={"Select Command"}
action={"select"} identifier={"id"} display={"display"} />}
/>
{openFilterOptionsDialog &&
<MythicDialog fullWidth={true} maxWidth="md" open={openFilterOptionsDialog}
onClose={() => {
setOpenFilterOptionsDialog(false);
}}
innerDialog={<CallbacksTabsTaskingFilterDialog
filterCommandOptions={loadedOptions.current} onSubmit={props.onSubmitFilter}
filterOptions={props.filterOptions} onClose={() => {
setOpenFilterOptionsDialog(false);
}}/>}
/>
}
</React.Fragment>
{openSelectCommandDialog &&
<MythicDialog fullWidth={true} maxWidth="md" open={openSelectCommandDialog}
onClose={() => {
setOpenSelectCommandDialog(false)
}}
innerDialog={<MythicSelectFromListDialog onClose={() => {
setOpenSelectCommandDialog(false);
}}
onSubmit={processCommandAndCommandLine}
options={commandOptions.current}
title={"Select Command"}
action={"select"} identifier={"id"}
display={"display"}/>}
/>
}
</div>
);
}
export const CallbacksTabsTaskingInput = React.memo(CallbacksTabsTaskingInputPreMemo);
@@ -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"
@@ -14,9 +14,8 @@ import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip'
import {taskingDataFragment, createTaskingMutation} from './CallbackMutations.js';
import Split from 'react-split';
import {TaskDisplayContainerFlat} from "./TaskDisplayContainer";
import {DrawTaskElementsFlowWithProvider} from "./C2PathDialog";
import {useTheme} from '@mui/material/styles';
import { validate as uuidValidate } from 'uuid';
import {getSkewedNow} from "../../utilities/Time";
export function CallbacksTabsTaskingSplitLabel(props){
@@ -51,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={
@@ -83,14 +82,14 @@ 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);
const [commandInfo, setCommandInfo] = React.useState({});
const [taskingData, setTaskingData] = React.useState({task: []});
const taskingDataRef = React.useRef({task: []});
const [fromNow, setFromNow] = React.useState((new Date()).toISOString());
const [fromNow, setFromNow] = React.useState(getSkewedNow().toISOString());
const [selectedToken, setSelectedToken] = React.useState({});
const [filterOptions, setFilterOptions] = React.useState({
"operatorsList": [],
@@ -172,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) => {
@@ -375,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;
@@ -412,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}`}>
@@ -444,14 +447,21 @@ 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}
/>
</div>
</Split>
<CallbacksTabsTaskingInput filterTasks={true} me={me} onSubmitFilter={onSubmitFilter}
payloadtype_name={tabInfo.payloadtype}
payloadtype_name={tabInfo.payloadtype} focus={index === value}
onSubmitCommandLine={onSubmitCommandLine} changeSelectedToken={changeSelectedToken}
filterOptions={filterOptions} callback_id={tabInfo.callbackID}
callback_os={tabInfo.os} parentMountedRef={mountedRef} />
@@ -468,44 +478,12 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
</MythicTabPanel>
);
}
const CallbacksTabsTaskingSplitTable = ({selectedTask, me, changeSelectedTask, onSelectTask}) => {
const [edges, setEdges] = React.useState([]);
const theme = useTheme();
useEffect(() => {
let initialEdge = {
source: {...selectedTask, selected: true, onSelectTask },
destination: {...selectedTask, selectedTask: true, onSelectTask},
}
const newEdges = selectedTask?.tasks?.reduce( (prev, cur) => {
let nextEdge = {
source: {...selectedTask, selected: false, onSelectTask},
destination: {...cur, selected: false, onSelectTask},
}
return [...prev, nextEdge];
}, [initialEdge]) || [initialEdge];
setEdges(newEdges);
}, [selectedTask]);
const CallbacksTabsTaskingSplitTable = ({selectedTask, me}) => {
return (
<Split direction="vertical" style={{width: "100%", height: "100%", overflowY: "auto", display: "flex", flexDirection: "column"}} minSize={[0,0]} sizes={[0, 100]} >
<div className="bg-gray-base" style={{display: "inline-flex", height: "100%", width: "100%"}}>
{
selectedTask.id > 0 &&
<DrawTaskElementsFlowWithProvider theme={theme} edges={edges} view_config={{group_by: "name", rankDir: "LR",}} />
}
</div>
<div className="bg-gray-light" style={{display: "inline-flex", height: "100%", width: "100%", flexDirection: "column"}}>
<div style={{width: "100%", height: "100%", overflowY: "auto", display: "flex", flexDirection: "column"}} >
{selectedTask.id > 0 &&
<TaskDisplayContainerFlat me={me} task={selectedTask} />
<TaskDisplayContainerFlat key={selectedTask.id} me={me} task={selectedTask} />
}
</div>
</Split>
</div>
)
}
/*
{selectedTask.id > 0 &&
<TaskDisplayFlat key={"taskinteractdisplaytable" + selectedTask.id} me={me} task={selectedTask}
command_id={selectedTask.command == null ? 0 : selectedTask.command.id}
filterOptions={filterOptions} showOnSelectTask={false} onSelectTask={onSelectTask}/>
}
*/
}
@@ -1,8 +1,8 @@
import React, {createContext} from 'react';
import {useSubscription, gql, useQuery, useLazyQuery } from '@apollo/client';
import {CallbacksTable, CallbacksTableMaterialReactTable} from './CallbacksTable';
import {useSubscription, gql, useQuery } from '@apollo/client';
import {CallbacksTable} from './CallbacksTable';
import {CallbacksGraph} from './CallbacksGraph';
import {useMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
import {getSkewedNow} from "../../utilities/Time";
export const CallbackGraphEdgesContext = createContext([]);
export const OnOpenTabContext = createContext( () => {});
export const OnOpenTabsContext = createContext( () => {});
@@ -13,8 +13,11 @@ const SUB_Callbacks = gql`
subscription CallbacksSubscription($fromNow: timestamp!){
callback_stream(batch_size: 1000, cursor: {initial_value: {timestamp: $fromNow}}) {
architecture
cwd
impersonation_context
active
dead
color
display_id
description
domain
@@ -39,6 +42,24 @@ callback_stream(batch_size: 1000, cursor: {initial_value: {timestamp: $fromNow}}
last_checkin
current_time
mythictree_groups
trigger_on_checkin_after_time
tags {
tagtype {
name
color
id
}
id
}
callbackports(where: {deleted: {_eq: false}}) {
local_port
remote_port
remote_ip
port_type
username
password
id
}
payload {
os
payloadtype {
@@ -56,8 +77,11 @@ export const Query_Callbacks_And_Edges = gql`
query InitialActiveCallbacksQuery{
callback(where: {active: {_eq: true}}) {
architecture
cwd
impersonation_context
active
dead
color
display_id
description
domain
@@ -82,6 +106,24 @@ callback(where: {active: {_eq: true}}) {
last_checkin
current_time
mythictree_groups
trigger_on_checkin_after_time
tags {
tagtype {
name
color
id
}
id
}
callbackports(where: {deleted: {_eq: false}}) {
id
local_port
remote_port
remote_ip
port_type
username
password
}
payload {
os
payloadtype {
@@ -109,6 +151,14 @@ callback(where: {active: {_eq: true}}) {
process_name
integrity_level
extra_info
tags {
tagtype {
name
color
id
}
id
}
payload {
payloadtype {
name
@@ -118,6 +168,8 @@ callback(where: {active: {_eq: true}}) {
callbackc2profiles {
c2profile {
name
has_logo
is_p2p
}
}
}
@@ -134,6 +186,14 @@ callback(where: {active: {_eq: true}}) {
process_name
integrity_level
extra_info
tags {
tagtype {
name
color
id
}
id
}
payload {
payloadtype {
name
@@ -143,6 +203,8 @@ callback(where: {active: {_eq: true}}) {
callbackc2profiles {
c2profile {
name
has_logo
is_p2p
}
}
}
@@ -150,6 +212,7 @@ callback(where: {active: {_eq: true}}) {
id
is_p2p
name
has_logo
}
}
}
@@ -162,6 +225,7 @@ callbackgraphedge_stream(batch_size: 100, cursor: {initial_value: {updated_at: $
destination {
active
id
color
display_id
operation_id
user
@@ -172,6 +236,14 @@ callbackgraphedge_stream(batch_size: 100, cursor: {initial_value: {updated_at: $
process_name
integrity_level
extra_info
tags {
tagtype {
name
color
id
}
id
}
payload {
payloadtype {
name
@@ -181,12 +253,15 @@ callbackgraphedge_stream(batch_size: 100, cursor: {initial_value: {updated_at: $
callbackc2profiles {
c2profile {
name
has_logo
is_p2p
}
}
}
source {
active
id
color
display_id
operation_id
user
@@ -197,6 +272,14 @@ callbackgraphedge_stream(batch_size: 100, cursor: {initial_value: {updated_at: $
process_name
integrity_level
extra_info
tags {
tagtype {
name
color
id
}
id
}
payload {
payloadtype {
name
@@ -206,6 +289,8 @@ callbackgraphedge_stream(batch_size: 100, cursor: {initial_value: {updated_at: $
callbackc2profiles {
c2profile {
name
has_logo
is_p2p
}
}
}
@@ -213,6 +298,7 @@ callbackgraphedge_stream(batch_size: 100, cursor: {initial_value: {updated_at: $
id
is_p2p
name
has_logo
}
}
}
@@ -221,7 +307,7 @@ export function CallbacksTop(props){
const me = props.me;
const callbacks = React.useRef([]);
const callbackEdges = React.useRef([]);
const fromNow = React.useRef(new Date());
const fromNow = React.useRef(getSkewedNow());
const mountedRef = React.useRef(true);
const [loading, setLoading] = React.useState(true);
useSubscription(SUB_Callbacks, {
@@ -298,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,
@@ -309,7 +395,10 @@ export function CallbacksTop(props){
payloadDescription: callbacks.current[i]["payload"]["description"],
callbackDescription: callbacks.current[i]["description"],
host: callbacks.current[i]["host"],
os: callbacks.current[i]["payload"]["os"]};
color: callbacks.current[i]["color"],
os: callbacks.current[i]["payload"]["os"],
customBrowser: customBrowser,
};
props.onOpenTab(tabData);
}
}
@@ -330,7 +419,10 @@ export function CallbacksTop(props){
payloadDescription: callbacks.current[i]["payload"]["description"],
callbackDescription: callbacks.current[i]["description"],
host: callbacks.current[i]["host"],
os: callbacks.current[i]["payload"]["os"]};
color: callbacks.current[i]["color"],
os: callbacks.current[i]["payload"]["os"],
customBrowser: tabs[j].customBrowser,
};
newTabData.push(tabData);
}
}
@@ -28,14 +28,28 @@ import {Button, Link, IconButton} from '@mui/material';
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
import MenuBookIcon from '@mui/icons-material/MenuBook';
import InfoIconOutline from '@mui/icons-material/InfoOutlined';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {HostFileDialog} from "../Payloads/HostFileDialog";
import PublicIcon from '@mui/icons-material/Public';
import {payloadsCallbackAllowed} from "../Payloads/Payloads";
import Switch from '@mui/material/Switch';
const GET_Payload_Details = gql`
query GetCallbackDetails($callback_id: Int!) {
callback_by_pk(id: $callback_id){
tags {
tagtype {
name
color
id
}
id
}
payload {
uuid
id
creation_time
callback_allowed
payloadtype{
name
agent_type
@@ -142,6 +156,8 @@ query GetCallbackDetails($callback_id: Int!) {
process_name
init_callback
mythictree_groups
cwd
impersonation_context
}
}
@@ -169,6 +185,7 @@ mutation removeLoadedCommand($id: Int!){
export function DetailedCallbackTable(props){
const theme = useTheme();
const me = useReactiveVar(meState);
const [openHostDialog, setOpenHostDialog] = React.useState(false);
const [openDetailedView, setOpenDetailedView] = React.useState(false);
const [openAddRemoveCommandsDialog, setOpenAddRemoveCommandsDialog] = React.useState(false);
const [commands, setCommands] = React.useState([]);
@@ -206,6 +223,7 @@ export function DetailedCallbackTable(props){
issueNextMod();
}
})
const [payloadCallbackAllowed, setPayloadCallbackAllowed] = React.useState(true);
const issueNextMod = () => {
if(commandMods.current.add >= addTotal.current){
if(commandMods.current.remove >= removeTotal.current) {
@@ -282,8 +300,29 @@ export function DetailedCallbackTable(props){
}];
}, []);
setC2Profiles(c2ProfilesState);
setPayloadCallbackAllowed(data.callback_by_pk.payload.callback_allowed);
}
});
const [toggleCallbackAllowedMutation] = useMutation(payloadsCallbackAllowed, {
onCompleted: (data) => {
if(data.updatePayload.status === "success"){
setPayloadCallbackAllowed(data.updatePayload.callback_allowed);
if(data.updatePayload.callback_allowed){
snackActions.success("New Callbacks allowed from this Payload");
} else {
snackActions.warning("No new Callback allowed from this Payload");
}
} else {
console.log(data.updatePayload);
}
},
onError: (data) => {
console.log(data);
}
});
const onToggleCallbackAllowed = () => {
toggleCallbackAllowedMutation({variables: {payload_uuid: data.callback_by_pk.payload.uuid, callback_allowed: !payloadCallbackAllowed}})
}
if (loading) {
return <LinearProgress style={{marginTop: "10px"}}/>;
}
@@ -351,7 +390,22 @@ export function DetailedCallbackTable(props){
) : null }
<TableRow hover>
<TableCell>Download URL</TableCell>
<TableCell>{window.location.origin + "/direct/download/" + data.callback_by_pk.payload.filemetum.agent_file_id}</TableCell>
<TableCell style={{display: "flex", alignItems: "center"}}>
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" href={"/direct/download/" + data.callback_by_pk.payload.filemetum.agent_file_id}>
{window.location.origin + "/direct/download/" + data.callback_by_pk.payload.filemetum.agent_file_id}
</Link>
<MythicStyledTooltip title={"Host Payload Through C2"} >
<PublicIcon color={"info"} style={{marginLeft: "20px", cursor: "pointer"}} onClick={()=>{setOpenHostDialog(true);}} />
</MythicStyledTooltip>
{openHostDialog &&
<MythicDialog fullWidth={true} maxWidth="md" open={openHostDialog}
onClose={()=>{setOpenHostDialog(false);}}
innerDialog={<HostFileDialog file_uuid={data.callback_by_pk.payload.filemetum.agent_file_id}
file_name={b64DecodeUnicode(data.callback_by_pk.payload.filemetum.filename_text)}
onClose={()=>{setOpenHostDialog(false);}} />}
/>
}
</TableCell>
</TableRow>
<TableRow hover>
<TableCell>SHA1</TableCell>
@@ -365,6 +419,18 @@ export function DetailedCallbackTable(props){
<TableCell>Created By</TableCell>
<TableCell>{data.callback_by_pk.payload?.operator?.username}</TableCell>
</TableRow>
<TableRow hover>
<TableCell>New Callbacks Allowed?</TableCell>
<TableCell>
<Switch
checked={payloadCallbackAllowed}
onChange={onToggleCallbackAllowed}
color="info"
inputProps={{'aria-label': 'primary checkbox'}}
name="callback_allowed"
/>
</TableCell>
</TableRow>
{data.callback_by_pk.payload?.eventstepinstance &&
<>
<TableRow hover>
@@ -17,12 +17,14 @@ import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
export function DownloadHistoryDialog(props){
const [history, setHistory] = React.useState([]);
const [openPreviewMediaDialog, setOpenPreviewMediaDialog] = React.useState(false);
const previewMediaRef = React.useRef();
const onPreviewMedia = (event, hist) => {
if(event){
event.preventDefault();
event.stopPropagation();
}
setOpenPreviewMediaDialog(true);
previewMediaRef.current = hist;
}
useEffect( () => {
setHistory(props.value);
@@ -37,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>
@@ -53,16 +55,8 @@ export function DownloadHistoryDialog(props){
height: "25px", marginRight: "5px", position: "relative", cursor: "pointer", display: "inline-block"}}
onClick={(e) => onPreviewMedia(e, hist)} />
</MythicStyledTooltip>
{openPreviewMediaDialog &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openPreviewMediaDialog}
onClose={(e)=>{setOpenPreviewMediaDialog(false);}}
innerDialog={<PreviewFileMediaDialog
agent_file_id={hist.agent_file_id}
filename={hist.filename_text}
onClose={(e)=>{setOpenPreviewMediaDialog(false);}} />}
/>
}
{props.deleted ? (
{hist.deleted ? (
<Typography variant="body2" style={{wordBreak: "break-all"}}>{hist.full_remote_path_text === "" ? hist.filename_text : hist.full_remote_path_text}</Typography>
) : (
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" href={"/direct/download/" + hist.agent_file_id}>{hist.full_remote_path_text === "" ? hist.filename_text : hist.full_remote_path_text}</Link>
@@ -74,20 +68,35 @@ export function DownloadHistoryDialog(props){
</TableCell>
<TableCell>{hist.timestamp}</TableCell>
<TableCell>
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
href={"/new/task/" + hist.task.display_id}>
{hist.task.display_id}
</Link>
{hist.task &&
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
href={"/new/task/" + hist.task.display_id}>
T-{hist.task.display_id}
</Link>
}
</TableCell>
<TableCell>
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
href={"/new/callbacks/" + hist.task.callback.display_id}>
{hist.task.callback.display_id}
</Link>
{hist.task &&
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
href={"/new/callbacks/" + hist.task.callback.display_id}>
C-{hist.task.callback.display_id}
</Link>
}
</TableCell>
<TableCell>{hist.comment}</TableCell>
</TableRow>
))}
{openPreviewMediaDialog &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openPreviewMediaDialog}
onClose={(e)=>{setOpenPreviewMediaDialog(false);}}
innerDialog={<PreviewFileMediaDialog
agent_file_id={previewMediaRef.current?.agent_file_id}
filename={previewMediaRef.current?.filename_text}
onClose={(e)=>{setOpenPreviewMediaDialog(false);}} />}
/>
}
</TableBody>
</Table>
</TableContainer>
@@ -14,9 +14,10 @@ import {MythicStyledTooltip} from '../../MythicComponents/MythicStyledTooltip';
import Pagination from '@mui/material/Pagination';
import {ResponseDisplayInteractive} from "./ResponseDisplayInteractive";
import {ResponseDisplayMedia} from "./ResponseDisplayMedia";
import {useMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
import {DrawBrowserScriptElementsFlow} from "./C2PathDialog";
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!){
@@ -78,7 +79,7 @@ export function b64DecodeUnicode(str) {
bytes[i] = text.charCodeAt(i);
}
const decoder = new TextDecoder(); // default is utf-8
return decodeURIComponent(decoder.decode(bytes));
return decoder.decode(bytes);
}catch(error){
try{
return decodeURIComponent(window.atob(str));
@@ -96,24 +97,30 @@ 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 taskID = React.useRef(props.task.id);
const seenResponseIDs = React.useRef([]);
const search = React.useRef("");
const [totalCount, setTotalCount] = React.useState(0);
const [openBackdrop, setOpenBackdrop] = React.useState(true);
const initialResponseStreamLimit = useMythicSetting({setting_name: "experiment-responseStreamLimit", default_value: 50, output: "number"})
const togglingAllOutputToPaginated = React.useRef(false);
const initialResponseStreamLimit = GetMythicSetting({setting_name: "experiment-responseStreamLimit", default_value: operatorSettingDefaults["experiment-responseStreamLimit"]});
const [fetchMoreResponses] = useLazyQuery(getResponsesLazyQuery, {
fetchPolicy: "network-only",
onCompleted: (data) => {
//console.log("fetchMoreResponses called", data)
data.response.forEach( (r) => {
if(!seenResponseIDs.current.includes(r.id)){
seenResponseIDs.current.push(r.id);
}
})
// set raw responses to be what we just manually fetched
const responseArray = data.response.map( r =>{ return {...r, response: b64DecodeUnicode(r.response)}});
setRawResponses(responseArray);
@@ -123,7 +130,6 @@ const NonInteractiveResponseDisplay = (props) => {
}, b64DecodeUnicode(""));
setOutput(responses);
// update maxID
if(!props.selectAllOutput){
setTotalCount(data.response_aggregate.aggregate.count);
}
@@ -146,6 +152,7 @@ const NonInteractiveResponseDisplay = (props) => {
setTotalCount(1);
setOpenBackdrop(false);
togglingAllOutputToPaginated.current = true;
},
onError: (data) => {
@@ -161,44 +168,48 @@ const NonInteractiveResponseDisplay = (props) => {
}else{
fetchAllResponses({variables: {task_id: props.task.id, search: "%" + search.current + "%"}})
}
}else{
}else if(togglingAllOutputToPaginated.current){
// going from select all output to not select all output
// don't fetch this on first load
onSubmitPageChange(1);
togglingAllOutputToPaginated.current = false;
}
//}
}, [props.selectAllOutput]);
}, [props.selectAllOutput, togglingAllOutputToPaginated.current]);
React.useEffect( () => {
setOpenBackdrop(true);
onSubmitPageChange(1);
setOutput("");
setRawResponses([]);
setTotalCount(0);
//onSubmitPageChange(1);
}, [props.task.id]);
const subscriptionDataCallback = React.useCallback( ({data}) => {
setTimeout(() => {
// close the backdrop after 2 seconds in case there's no data to fetch
setOpenBackdrop(false);
}, 1000);
const subscriptionDataCallback = ({data}) => {
//console.log("fetchLimit", fetchLimit, "totalCount", totalCount);
if(props.task.id !== taskID.current){
console.log("props.task.id !== taskID.current", props.task.id, taskID.current)
taskID.current = props.task.id;
// this is the latest batch of responses
// base64 decode all of the response data
const responseArray = data.data.response_stream.map( r =>{ return {...r, response: b64DecodeUnicode(r.response)}});
// set the aggregated output
const responses = responseArray.reduce( (prev, cur) => {
return prev + cur.response;
}, b64DecodeUnicode(""));
setOutput(responses);
setRawResponses(responseArray);
setTotalCount(responseArray.length);
setOpenBackdrop(false);
} else {
if(totalCount >= initialResponseStreamLimit && initialResponseStreamLimit > 0){
if(rawResponses.length >= initialResponseStreamLimit && initialResponseStreamLimit > 0 && !props.selectAllOutput){
// we won't display it
console.log("got more than we can see currently", totalCount);
setOpenBackdrop(false);
let newTotal = totalCount;
data.data.response_stream.forEach( (r) => {
if(!seenResponseIDs.current.includes(r.id)){
newTotal += 1;
seenResponseIDs.current.push(r.id);
}
})
setTotalCount(newTotal);
return;
}
// we still have some room to view more, but only room for initialResponseStreamLimit - totalFetched.current
let newTotal = totalCount;
const newerResponses = data.data.response_stream.reduce( (prev, cur) => {
if(!seenResponseIDs.current.includes(cur.id)){
newTotal += 1;
seenResponseIDs.current.push(cur.id);
}
let prevIndex = prev.findIndex( (v,i,a) => v.id === cur.id);
if(prevIndex >= 0){
prev[prevIndex] = {...cur, response: b64DecodeUnicode(cur.response)};
@@ -208,8 +219,9 @@ const NonInteractiveResponseDisplay = (props) => {
}, rawResponses);
// sort them to make sure we're still in order
newerResponses.sort( (a,b) => a.id > b.id ? 1 : -1);
setTotalCount(newTotal);
// newerResponses is everything we've seen plus everything new
if(initialResponseStreamLimit > 0){
if(initialResponseStreamLimit > 0 && !props.selectAllOutput){
// take just the responses that make up our stream limit
const finalRawResponses = newerResponses.slice(0, initialResponseStreamLimit);
const outputResponses = finalRawResponses.reduce( (prev, cur) => {
@@ -227,8 +239,8 @@ const NonInteractiveResponseDisplay = (props) => {
setOutput(outputResponses);
}
setOpenBackdrop(false);
}
}, [output, rawResponses, totalCount, props.task.id]);
};
useSubscription(subResponsesStream, {
variables: {task_id: props.task.id},
fetchPolicy: "network_only",
@@ -280,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"}
@@ -303,7 +318,7 @@ const NonInteractiveResponseDisplay = (props) => {
</div>
}
</React.Fragment>
</>
)
}
export const ResponseDisplayConsole = (props) => {
@@ -354,15 +369,9 @@ export const NonInteractiveResponseDisplayConsole = (props) => {
fetchPolicy: "network_only",
onData: subscriptionDataCallback
});
const onSubmitSearch = React.useCallback( (newSearch) => {
snackActions.warning("Search not supported for console view");
}, []);
return (
<React.Fragment>
{props.searchOutput &&
<SearchBar onSubmitSearch={onSubmitSearch}/>
}
<div style={{overflowY: "auto", width: "100%", height: props.expand ? "100%" : undefined}}
ref={props.responseRef}>
<ResponseDisplayComponent rawResponses={rawResponses} viewBrowserScript={props.viewBrowserScript}
@@ -399,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 (
@@ -439,7 +448,6 @@ const ResponseDisplayComponent = ({rawResponses, viewBrowserScript, output, comm
const [localViewBrowserScript, setViewBrowserScript] = React.useState(true);
const [browserScriptData, setBrowserScriptData] = React.useState({});
const [loadingBrowserScript, setLoadingBrowserScript] = React.useState(true);
const useNewBrowserScriptTable = useMythicSetting({setting_name: "experiment-browserscripttable", default_value: "false"});
const script = React.useRef(undefined);
const filterOutput = (scriptData) => {
if(search === ""){
@@ -452,7 +460,6 @@ const ResponseDisplayComponent = ({rawResponses, viewBrowserScript, output, comm
copied["plaintext"] = "";
}
}
if(useNewBrowserScriptTable){return copied}
if(scriptData["table"] !== undefined){
if(scriptData["table"].length > 0){
copied["table"] = scriptData.table.map(t => {
@@ -539,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} />
)) || null
}
{browserScriptData?.plaintext !== undefined &&
<ResponseDisplayPlaintext plaintext={browserScriptData["plaintext"]} task={task}
expand={expand} displayType={displayType} />
}
{browserScriptData?.table?.map( (table, index) => {
return <ResponseDisplayTable callback_id={task.callback_id} task={task} expand={expand}
table={table} key={"tablefortask" + task.id + "table" + index}
displayType={displayType}
/>
}) || null
}
{browserScriptData?.download?.map( (dl, index) => (
<ResponseDisplayDownload download={dl} task={task} displayType={displayType}
key={"download" + index + "fortask" + task.id} />
)) || null
}
{browserScriptData?.search?.map( (s, index) => (
<ResponseDisplaySearch search={s} task={task} displayType={displayType}
key={"searchlink" + index + "fortask" + task.id} />
)) || null
}
{browserScriptData?.media?.map( (s, index) => (
<ResponseDisplayMedia key={"searchmedia" + index + "fortask" + task.id}
displayType={displayType}
task={task} media={s} expand={expand} />
)) || null}
{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}
@@ -44,8 +45,9 @@ const getTaskingStatus = (task) => {
return <CheckCircleOutlineIcon color={"success"} fontSize={"1rem"} style={{marginRight: "2px"}} />
} else if(task.status === "submitted"){
return <CircularProgress size={"1rem"} />
} else if(task.status.toLowerCase.includes("error")){
} else {
console.log(task.status);
return null
}
}
const getClassnames = (entry) => {
@@ -147,7 +149,6 @@ export const GetOutputFormatAll = ({data, myTask, taskID, useASNIColor, message
}
})
setDataElement(elements);
}, [data, useASNIColor, showTaskStatus, wrapText]);
React.useLayoutEffect( () => {
if(myTask){
@@ -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",
@@ -291,20 +292,21 @@ export const ResponseDisplayInteractive = (props) =>{
if(search === undefined || search === ""){
let allData = [...rawResponses, ...taskData];
allData.sort( (a,b) => {
let aDate = new Date(a.timestamp);
let bDate = new Date(b.timestamp);
let aDate = new Date(a?.status_timestamp_preprocessing || a.timestamp);
let bDate = new Date(b?.status_timestamp_preprocessing || b.timestamp);
return aDate < bDate ? -1 : bDate < aDate ? 1 : 0;
})
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];
@@ -316,8 +318,8 @@ export const ResponseDisplayInteractive = (props) =>{
}
})
allData.sort( (a,b) => {
let aDate = new Date(a.timestamp);
let bDate = new Date(b.timestamp);
let aDate = new Date(a?.status_timestamp_preprocessing || a.timestamp);
let bDate = new Date(b?.status_timestamp_preprocessing || b.timestamp);
return aDate < bDate ? -1 : bDate < aDate ? 1 : 0;
});
@@ -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,69 +468,74 @@ 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);
setTaskOptions(newTaskOptions);
}, [taskData]);
const onInputChange = (name, value, error, event) => {
if(event.key === "ArrowUp"){
event.preventDefault();
event.stopPropagation();
if(taskOptions.length === 0){
snackActions.warning("No previous tasks")
return;
}
let newIndex = (taskOptionsIndex + 1);
if(newIndex > taskOptions.length -1){
newIndex = taskOptions.length -1;
}
setTaskOptionsIndex(newIndex);
setInputText(taskOptions[newIndex].display_params.trim());
}else if(event.key === "ArrowDown"){
event.preventDefault();
event.stopPropagation();
if(taskData.length === 0){
snackActions.warning("No next tasks")
return;
}
let newIndex = (taskOptionsIndex -1);
if(newIndex < 0){
setTaskOptionsIndex(-1);
setInputText("");
} else {
if(event !== null && event !== undefined){
if(event.key === "ArrowUp"){
event.preventDefault();
event.stopPropagation();
if(taskOptions.length === 0){
snackActions.warning("No previous tasks")
return;
}
let newIndex = (taskOptionsIndex + 1);
if(newIndex > taskOptions.length -1){
newIndex = taskOptions.length -1;
}
setTaskOptionsIndex(newIndex);
setInputText(taskOptions[newIndex].display_params.trim());
}
}else if(event.key === "ArrowDown"){
event.preventDefault();
event.stopPropagation();
if(taskData.length === 0){
snackActions.warning("No next tasks")
return;
}
let newIndex = (taskOptionsIndex -1);
if(newIndex < 0){
setTaskOptionsIndex(-1);
setInputText("");
} else {
setTaskOptionsIndex(newIndex);
setInputText(taskOptions[newIndex].display_params.trim());
}
}else{
}else{
setInputText(value);
}
} else {
setInputText(value);
}
}
const submitTask = (event) => {
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 = "";
}
@@ -530,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,
@@ -547,7 +565,7 @@ const InteractiveTaskingBar = ({
}})
}
setInputText("");
setSelectedControlOption(InteractiveMessageTypes[0]);
setSelectedControlOption(0);
setTaskOptionsIndex(-1);
}
const onChangeSelect = (event) => {
@@ -564,85 +582,96 @@ 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>
<MythicTextField autoFocus={true} maxRows={5} multiline={true} onChange={onInputChange} onEnter={submitTask}
value={inputText} variant={"standard"} placeholder={">_ type here..."} inline={true}
marginBottom={"0px"} InputProps={{style: { width: "100%"}}}/>
<FormControl style={{width: "6rem"}} >
debounceDelay={50}
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}}>
<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}}>
<IconButton onClick={toggleShowTaskStatus} style={{paddingLeft: 0, paddingRight: 0}} disableRipple={true} disableFocusRipple={true}>
<CheckCircleOutlineIcon color={showTaskStatus ? "success" : "secondary"}
style={{cursor: "pointer",}}
/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={wrapText ? "Unwrap Text" : "Wrap Text"} >
<IconButton onClick={toggleWrapText} style={{paddingLeft: 0, paddingRight: 0}}>
<IconButton onClick={toggleWrapText} style={{paddingLeft: 0, paddingRight: 0}} disableRipple={true} disableFocusRipple={true}>
<WrapTextIcon color={wrapText ? "success" : "secondary"}
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(expand){
return (
<div style={{height: "50px"}}>
</div>
)
} else {
return null
}
if(pageCount < 2 || selectAllOutput){
return null;
}
return (
<div style={{background: "transparent", display: "flex", justifyContent: "center", alignItems: "center"}} >
@@ -1,642 +0,0 @@
import React, {useEffect, useRef} from 'react';
import {Button} from '@mui/material';
import { MythicViewJSONAsTableDialog, MythicDialog } from '../../MythicComponents/MythicDialog';
import { MythicDisplayTextDialog } from '../../MythicComponents/MythicDisplayTextDialog';
import { ResponseDisplayTableDialogTable } from './ResponseDisplayTableDialogTable';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import {useTheme} from '@mui/material/styles';
import 'react-virtualized/styles.css';
import {TaskFromUIButton} from './TaskFromUIButton';
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 { copyStringToClipboard } from '../../utilities/Clipboard';
import {snackActions} from '../../utilities/Snackbar';
import {MythicStyledTooltip} from '../../MythicComponents/MythicStyledTooltip';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faList, faTrashAlt, faSkullCrossbones, faCamera, faSyringe, faFolder, faFolderOpen, faFileArchive, faCog, faFileWord, faFileExcel, faFilePowerpoint, faFilePdf, faDatabase, faKey, faFileCode, faDownload, faUpload, faFileImage, faCopy, faBoxOpen, faFileAlt } from '@fortawesome/free-solid-svg-icons';
// --------
import {
MaterialReactTable,
useMaterialReactTable,
} from 'material-react-table';
const onCopyToClipboard = (data) => {
let result = copyStringToClipboard(data);
if(result){
snackActions.success("Copied text!");
}else{
snackActions.error("Failed to copy text");
}
}
const getIconName = (iconName) => {
switch(iconName.toLowerCase()){
case "openfolder":
case "folder":
return faFolderOpen;
case "closedfolder":
return faFolder;
case "archive":
case "zip":
return faFileArchive;
case "diskimage":
return faBoxOpen;
case "executable":
return faCog;
case "word":
return faFileWord;
case "excel":
return faFileExcel;
case "powerpoint":
return faFilePowerpoint;
case "pdf":
case "adobe":
return faFilePdf;
case "database":
return faDatabase;
case "key":
return faKey;
case "code":
case "source":
return faFileCode;
case "download":
return faDownload;
case "upload":
return faUpload;
case "png":
case "jpg":
case "image":
return faFileImage;
case "list":
return faList;
case "delete":
return faTrashAlt;
case "inject":
return faSyringe;
case "kill":
return faSkullCrossbones;
case "camera":
return faCamera;
default:
return faFileAlt;
}
}
const ResponseDisplayTableStringCell = ({cellData, rowData}) => {
return (
<div style={{...cellData?.cellStyle}}>
{cellData?.startIcon?
<MythicStyledTooltip title={cellData?.startIconHoverText || ""} >
<FontAwesomeIcon icon={getIconName(cellData?.startIcon)} style={{marginRight: "5px", color: cellData?.startIconColor || ""}}/>
</MythicStyledTooltip>
: null
}
{cellData?.plaintextHoverText &&
<MythicStyledTooltip title={cellData.plaintextHoverText}>
<pre style={{display: "inline-block"}}>
{cellData?.plaintext?.replaceAll?.("\n", "") || cellData?.plaintext}
</pre>
</MythicStyledTooltip>
}
<pre style={{display: "inline-block"}}>
{cellData?.plaintext?.replaceAll?.("\n", "") || cellData?.plaintext}
</pre>
{ cellData?.endIcon &&
<MythicStyledTooltip title={cellData?.endIconHoverText || ""}>
<FontAwesomeIcon icon={getIconName(cellData?.endIcon)} style={{color: cellData?.endIconColor || ""}}/>
</MythicStyledTooltip>
}
</div>
);
}
const ResponseDisplayTableDateCell = ({cellData, rowData}) => {
return (
<div style={{...(cellData?.cellStyle || null)}}>
{cellData?.startIcon?
<MythicStyledTooltip title={cellData?.startIconHoverText || ""} >
<FontAwesomeIcon icon={getIconName(cellData?.startIcon)} style={{marginRight: "5px", color: cellData?.startIconColor || ""}}/>
</MythicStyledTooltip>
: null
}
{cellData?.plaintextHoverText? (
<MythicStyledTooltip title={cellData.plaintextHoverText}>
<pre style={{display: "inline-block"}}>
{cellData?.plaintext?.toISOString?.()}
</pre>
</MythicStyledTooltip>
) : (
<pre style={{display: "inline-block"}}>
{cellData?.plaintext?.toISOString?.()}
</pre>
)}
{cellData?.endIcon?
<MythicStyledTooltip title={cellData?.endIconHoverText || ""}>
<FontAwesomeIcon icon={getIconName(cellData?.endIcon)} style={{color: cellData?.endIconColor || ""}}/>
</MythicStyledTooltip>: null
}
</div>
);
}
const ResponseDisplayTableNumberCell = ({cellData, rowData}) => {
return (
<div style={{...(cellData?.cellStyle || null)}}>
{cellData?.startIcon?
<MythicStyledTooltip title={cellData?.startIconHoverText || ""} >
<FontAwesomeIcon icon={getIconName(cellData?.startIcon)} style={{marginRight: "5px", color: cellData?.startIconColor || ""}}/>
</MythicStyledTooltip>
: null
}
{cellData?.plaintextHoverText? (
<MythicStyledTooltip title={cellData.plaintextHoverText}>
<pre style={{display: "inline-block"}}>
{cellData?.plaintext || " "}
</pre>
</MythicStyledTooltip>
) : (
<pre style={{display: "inline-block"}}>
{cellData?.plaintext || " "}
</pre>
)}
{cellData?.endIcon?
<MythicStyledTooltip title={cellData?.endIconHoverText || ""}>
<FontAwesomeIcon icon={getIconName(cellData?.endIcon)} style={{color: cellData?.endIconColor || ""}}/>
</MythicStyledTooltip>: null
}
</div>
);
}
export const getStringSize = ({cellData}) => {
try{
// process for getting human readable string from bytes: https://stackoverflow.com/a/18650828
let bytes = parseInt(cellData["plaintext"]);
if (cellData["plaintext"] === ''){
return ""
}
if (bytes === 0){
return "0 Bytes";
}
const decimals = 2;
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', '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?.plaintext?.replaceAll?.("\n", "") || ""
}
}
const ResponseDisplayTableSizeCell = ({cellData, rowData}) => {
return (
<div style={{...(cellData?.cellStyle || null)}}>
{cellData?.plaintextHoverText? (
<MythicStyledTooltip title={cellData.plaintextHoverText} >
<pre style={{display: "inline-block"}}>
{getStringSize({cellData})}
</pre>
</MythicStyledTooltip>
) : (
<pre style={{display: "inline-block"}}>
{getStringSize({cellData})}
</pre>
)}
</div>
);
}
const ResponseDisplayTableActionCell = ({cellData, callback_id, rowData}) => {
const theme = useTheme();
const [openButton, setOpenButton] = React.useState(false);
const [openTaskingButton, setOpenTaskingButton] = React.useState(false);
const [openDictionaryButton, setOpenDictionaryButton] = React.useState(false);
const [openStringButton, setOpenStringButton] = React.useState(false);
const [openTableButton, setOpenTableButton] = React.useState(false);
const dropdownAnchorRef = useRef(null);
const [openDropdownButton, setOpenDropdownButton] = React.useState(false);
const [taskingData, setTaskingData] = React.useState({});
const handleClose = (event) => {
if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
return;
}
setOpenDropdownButton(false);
};
const handleMenuItemClick = (event, index) => {
switch(cellData.button.value[index].type.toLowerCase()){
case "task":
setTaskingData(cellData.button.value[index]);
setOpenTaskingButton(true);
break;
case "dictionary":
setTaskingData(cellData.button.value[index]);
setOpenDictionaryButton(true);
break;
case "string":
setTaskingData(cellData.button.value[index]);
setOpenStringButton(true);
break;
case "table":
setTaskingData(cellData.button.value[index]);
setOpenTableButton(true);
break;
}
setOpenDropdownButton(false);
};
const finishedTasking = () => {
setOpenButton(false);
setOpenTaskingButton(false);
setOpenDictionaryButton(false);
setOpenStringButton(false);
setOpenTableButton(false);
setTaskingData({});
}
const finishedViewingData = () => {
setOpenButton(false);
setOpenTaskingButton(false);
setOpenDictionaryButton(false);
setOpenStringButton(false);
setOpenTableButton(false);
setTaskingData({});
}
const getButtonObject = () => {
switch(cellData.button.type.toLowerCase()){
case "dictionary":
return (
<React.Fragment>
<MythicStyledTooltip title={cellData?.button?.hoverText || " "} >
<Button size="small" variant="contained" color="primary"
onClick={() => setOpenButton(true)} disabled={cellData?.button?.disabled || false}
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.startIconColor || ""}}/> : null}
>{cellData?.button?.name || " "}</Button>
</MythicStyledTooltip>
{openButton &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openButton}
onClose={()=>{setOpenButton(false);}}
innerDialog={<MythicViewJSONAsTableDialog title={cellData?.button?.title || "Title Here"} leftColumn={cellData?.button?.leftColumnTitle || "Left Column"}
rightColumn={cellData?.button?.rightColumnTitle || "Right Column"} value={cellData?.button?.value || {}} onClose={()=>{setOpenButton(false);}} />}
/>
}
</React.Fragment>
)
case "string":
return (
<React.Fragment>
<MythicStyledTooltip title={cellData?.button?.hoverText || " "} >
<Button size="small" variant="contained" color="primary"
onClick={() => setOpenButton(true)} disabled={cellData?.button?.disabled || false}
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.startIconColor || ""}}/> : null}
>{cellData?.button?.name || " "}</Button>
</MythicStyledTooltip>
{openButton &&
<MythicDisplayTextDialog fullWidth={true} maxWidth="lg" open={openButton} title={cellData?.button?.title || "Title Here"} value={cellData?.button?.value || ""}
onClose={()=>{setOpenButton(false);}}
/>
}
</React.Fragment>
)
case "table":
return (
<React.Fragment>
<MythicStyledTooltip title={cellData?.button?.hoverText || " "} >
<Button size="small" variant="contained" color="primary"
onClick={() => setOpenButton(true)} disabled={cellData?.button?.disabled || false}
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.startIconColor || ""}}/> : null}
>{cellData?.button?.name || " "}</Button>
</MythicStyledTooltip>
{openButton &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openButton}
onClose={()=>{setOpenButton(false);}}
innerDialog={<ResponseDisplayTableDialogTable title={cellData?.button?.title || "Title Here"}
table={cellData?.button?.value || {}} callback_id={callback_id} onClose={()=>{setOpenButton(false);}} />}
/>
}
</React.Fragment>
)
case "task":
return (
<React.Fragment>
<MythicStyledTooltip title={cellData?.button?.hoverText || " "}>
<Button size="small" onClick={() => setOpenTaskingButton(true)} disabled={cellData?.button?.disabled || false} variant="contained" color="warning"
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.startIconColor || ""}}/> : null}
>{cellData?.button?.name || " "}</Button>
</MythicStyledTooltip>
{openTaskingButton &&
<TaskFromUIButton ui_feature={cellData?.button?.ui_feature || " "}
callback_id={callback_id}
parameters={cellData?.button?.parameters || ""}
openDialog={cellData?.button?.openDialog || false}
getConfirmation={cellData?.button?.getConfirmation || false}
acceptText={cellData?.button?.acceptText || "confirm"}
selectCallback={cellData?.button?.selectCallback || false}
onTasked={() => setOpenTaskingButton(false)}/>
}
</React.Fragment>
)
case "menu":
return (
<React.Fragment>
{openTaskingButton &&
<TaskFromUIButton ui_feature={taskingData.ui_feature}
callback_id={callback_id}
parameters={taskingData.parameters}
openDialog={taskingData?.openDialog || false}
getConfirmation={taskingData?.getConfirmation || false}
acceptText={taskingData?.acceptText || "confirm"}
selectCallback={taskingData?.selectCallback || false}
onTasked={finishedTasking}/>
}
{openDictionaryButton &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openDictionaryButton}
onClose={finishedViewingData}
innerDialog={<MythicViewJSONAsTableDialog title={taskingData.title} leftColumn={taskingData.leftColumnTitle}
rightColumn={taskingData.rightColumnTitle} value={taskingData.value} onClose={finishedViewingData} />}
/>
}
{openStringButton &&
<MythicDisplayTextDialog fullWidth={true} maxWidth="lg" open={openStringButton} title={taskingData?.title || "Title Here"} value={taskingData?.value || ""}
onClose={finishedViewingData}
/>
}
{openTableButton &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openTableButton}
onClose={finishedViewingData}
innerDialog={<ResponseDisplayTableDialogTable title={taskingData?.title || "Title Here"}
table={taskingData?.value || {}} callback_id={callback_id} onClose={finishedViewingData} />}
/>
}
<Button size="small" variant="contained" color="primary" ref={dropdownAnchorRef}
onClick={() => setOpenDropdownButton(true)} disabled={cellData?.button?.disabled || false}
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.startIconColor || ""}}/> : null}
>{cellData?.button?.name || " "}</Button>
<Popper open={openDropdownButton} anchorEl={dropdownAnchorRef.current} role={undefined} transition style={{zIndex: 4}}>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
}}
>
<Paper variant="outlined" style={{backgroundColor: theme.palette.mode === 'dark' ? theme.palette.primary.dark : theme.palette.primary.light, color: "white"}}>
<ClickAwayListener onClickAway={handleClose}>
<MenuList id="split-button-menu" >
{cellData.button.value.map((option, index) => (
<MenuItem
key={option.name + index}
disabled={option.disabled}
onClick={(event) => handleMenuItemClick(event, index)}
>
<MythicStyledTooltip title={option?.hoverText || (option.type === "task" ? "Task an Agent" : "Display Data")}>
{option?.startIcon ? <FontAwesomeIcon icon={getIconName(option?.startIcon)} style={{color: option?.startIconColor || "", marginRight: "5px"}}/> : null}
{option.name}
</MythicStyledTooltip>
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</React.Fragment>
)
}
}
return (
<div style={{...(rowData?.rowStyle || null), ...(cellData?.cellStyle || null)}}>
{cellData?.plaintext ? cellData.plaintext : null}
{cellData?.button ? (getButtonObject()) : null}
</div>
);
}
export const ResponseDisplayMaterialReactTable = ({table, callback_id, expand, task}) =>{
const theme = useTheme();
const [allData, setAllData] = React.useState([]);
const filterOutButtonsFromRowData = (data) => {
let rowData = {};
for(const key of Object.keys(data)){
if(data[key]["plaintext"] !== undefined){
rowData[key] = data[key]["plaintext"];
}
}
return rowData;
}
const contextMenuOptions = [
{
name: 'Copy Row as JSON',
click: ({event, columnIndex, rowIndex, data}) => {
const filteredData = filterOutButtonsFromRowData(data);
onCopyToClipboard(JSON.stringify(filteredData, null, 2));
}
},
{
name: 'Copy Row as CSV',
click: ({event, columnIndex, rowIndex, data}) => {
const filteredData = filterOutButtonsFromRowData(data);
let outputHeaders = "";
let outputRow = "";
for(const key of Object.keys(filteredData)){
if(outputHeaders === ""){
outputHeaders += key;
} else {
outputHeaders += "," + key;
}
if(outputRow === ""){
outputRow += filteredData[key];
}else{
outputRow += "," + filteredData[key];
}
}
onCopyToClipboard(outputHeaders + "\n" + outputRow);
}
},
{
name: 'Copy Row as TSV',
click: ({event, columnIndex, rowIndex, data}) => {
const filteredData = filterOutButtonsFromRowData(data);
let outputHeaders = "";
let outputRow = "";
for(const key of Object.keys(filteredData)){
if(outputHeaders === ""){
outputHeaders += key;
} else {
outputHeaders += "\t" + key;
}
if(outputRow === ""){
outputRow += filteredData[key];
}else{
outputRow += "\t" + filteredData[key];
}
}
onCopyToClipboard(outputHeaders + "\n" + outputRow);
}
},
];
const columns = React.useMemo( () => table.headers.map( h => {
return {
accessorKey: `${h.plaintext}.plaintext`,
header: h.plaintext,
size: h.width,
id: h.plaintext,
enableClickToCopy: h.type !== 'button',
filterVariant: h.type === 'number' || h.type === 'size' ? 'range' : 'text',
enableResizing: h.type !== 'button',
enableSorting: h.type !== 'button',
enableColumnFilter: h.type !== 'button',
grow: h.fillWidth,
accessorFn: (row) => {
if(h.type === "date"){
return new Date(row[h.plaintext]?.plaintext || 0);
}
if(h.type === "number" || h.type === "size"){
try{
return Number(row[h.plaintext]?.plaintext);
}catch(error){
return row[h.plaintext]?.plaintext || "";
}
}
return row[h.plaintext]?.plaintext || "";
},
Cell: ({cell, renderedCellValue}) => {
let cellData = {...cell.row?.original?.[h.plaintext], plaintext: renderedCellValue};
switch(h.type){
case "string":
return (
<ResponseDisplayTableStringCell cellData={cellData} rowData={cell.row.original}/>
)
case "date":
return (
<ResponseDisplayTableDateCell cellData={cellData} rowData={cell.row.original}/>
)
case "size":
return (
<ResponseDisplayTableSizeCell cellData={cellData} rowData={cell.row.original}/>
)
case "button":
return (
<ResponseDisplayTableActionCell callback_id={callback_id} cellData={cellData} rowData={cell.row.original}/>
)
case "number":
return (
<ResponseDisplayTableNumberCell callback_id={callback_id} cellData={cellData} rowData={cell.row.original} />
)
default:
return (
<ResponseDisplayTableStringCell cellData={cellData} rowData={cell.row.original}/>
)
}
}
}
}), [table.headers])
useEffect( () => {
setAllData([...table.rows]);
}, [table.rows]);
const materialReactTable = useMaterialReactTable({
columns,
data: allData,
layoutMode: "grid",
autoResetPageIndex: false,
enableFacetedValues: true,
enablePagination: true,
enableBottomToolbar: true,
enableStickyHeader: true,
enableDensityToggle: false,
enableColumnResizing: true,
//enableColumnOrdering: true,
//columnResizeMode: 'onEnd',
defaultDisplayColumn: { enableResizing: true },
muiTableContainerProps: { sx: { alignItems: "flex-start" } },
mrtTheme: (theme) => ({
baseBackgroundColor: theme.palette.background.default, //change default background color
}),
muiSearchTextFieldProps: {
placeholder: 'Search loaded data',
size: 'small',
sx: { minWidth: '300px' },
variant: 'outlined',
},
muiTableHeadCellProps: {
sx: {
border: '1px solid rgba(81, 81, 81, .5)',
fontStyle: 'italic',
fontWeight: 'bold',
},
style: {
zIndex: 1,
}
},
muiTableHeadRowProps: {
sx: {}
},
muiTableBodyCellProps: ({ cell, table }) => {
return {
sx: {
...cell.row.original[cell.column.id]?.cellStyle,
padding: "0 0 0 10px",
}
}
},
muiTableBodyRowProps: ({ row }) => ({
sx: {
...row?.original?.rowStyle, padding: 0
},
style: {padding: 0}
}),
enableRowActions: false,
/*
renderRowActions: ({ row }) => (
<Box>
<IconButton onClick={() => console.info('Edit')}>
<EditIcon />
</IconButton>
<IconButton onClick={() => console.info('Delete')}>
<DeleteIcon />
</IconButton>
</Box>
),
*/
renderRowActionMenuItems: ({ row }) => contextMenuOptions.map( o => (
<MenuItem key={o.name} onClick={() => {o.click({data: row.original})}}>{o.name}</MenuItem>
)),
muiTablePaperProps: {
sx: { display: "flex", flexDirection: "column", width: "100%"}
},
muiTopToolbarProps: {
sx: {backgroundColor: theme.materialReactTableHeader},
},
renderTopToolbarCustomActions: () => (
<Typography variant="h5" >{table?.title || ""}</Typography>
),
renderEmptyRowsFallback: ({ table }) => (
<div style={{display: "flex", width: "100%", height: "100%", justifyContent: "center", flexDirection: "column", alignItems: "center"}}>
<Typography variant={"h4"} >
{"No Data"}
</Typography>
</div>
),
});
const scrollContent = (node, isAppearing) => {
// only auto-scroll if you issued the task
document.getElementById(`scrolltotaskbottom${task.id}`)?.scrollIntoView?.({
//behavior: "smooth",
block: "end",
inline: "nearest"
})
}
React.useLayoutEffect( () => {
scrollContent()
}, []);
return (
<MaterialReactTable table={materialReactTable} />
)
}
@@ -1,8 +1,7 @@
import React from 'react';
import {Typography, Link} from '@mui/material';
import { Button, IconButton } from '@mui/material';
import {useMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
import {modeOptions} from "../Search/PreviewFileStringDialog";
import {GetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
import MenuItem from '@mui/material/MenuItem';
import TextField from '@mui/material/TextField';
import AceEditor from 'react-ace';
@@ -42,19 +41,32 @@ import 'ace-builds/src-noconflict/mode-apache_conf';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import AppBar from '@mui/material/AppBar';
import Tabs from '@mui/material/Tabs';
import TabContext from '@mui/lab/TabContext';
import Tab from '@mui/material/Tab';
import {previewFileQuery} from "../Search/FileMetaTable";
import { useMutation, gql, useQuery } from '@apollo/client';
import CodeIcon from '@mui/icons-material/Code';
import DownloadIcon from '@mui/icons-material/Download';
import {b64DecodeUnicode} from "./ResponseDisplay";
import {TableRowSizeCell} from "../../MythicComponents/MythicDialog";
import {Table, TableHead, TableRow, TableBody} from '@mui/material';
import {MythicDialog, TableRowSizeCell} from "../../MythicComponents/MythicDialog";
import {Table, TableHead, TableRow, TableBody, TableCell, Paper} from '@mui/material';
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
import WarningOutlinedIcon from '@mui/icons-material/WarningOutlined';
import {TagsDisplay, ViewEditTags} from "../../MythicComponents/MythicTag";
import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery";
import {ResponseDisplayScreenshotModal} from "./ResponseDisplayScreenshotModal";
import initSQLJS from 'sql.js';
import TabPanel from '@mui/lab/TabPanel';
import TabList from '@mui/lab/TabList';
import {Backdrop, CircularProgress} from '@mui/material';
// Required to let webpack 4 know it needs to copy the wasm file to our assets
// eslint-disable-next-line import/no-webpack-loader-syntax
import sqlWasm from "!!file-loader?name=sql-wasm-[contenthash].wasm!sql.js/dist/sql-wasm.wasm";
import {copyStringToClipboard} from "../../utilities/Clipboard";
export const modeOptions = ["csharp", "golang", "html", "json", "markdown", "ruby", "python", "java",
"javascript", "yaml", "toml", "swift", "sql", "rust", "powershell", "pgsql", "perl", "php", "objectivec",
"nginx", "makefile", "kotlin", "dockerfile", "sh", "ini", "apache_conf"].sort();
const fileMetaQuery = gql`
query fileMetaStats($agent_file_id: String!){
filemeta(where: {agent_file_id: {_eq: $agent_file_id}}){
@@ -86,32 +98,31 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
const displayType = mimeType(media?.filename);
const [value, setValue] = React.useState(displayType !== undefined ? 0 : 1);
const [fileMetaData, setFileMetaData] = React.useState({});
useQuery(fileMetaQuery, {
variables: {agent_file_id: media?.agent_file_id},
fetchPolicy: "no-cache",
onCompleted: (data) => {
if(data.filemeta.length > 0){
setFileMetaData({
id: data.filemeta[0].id,
tags: data.filemeta[0].tags,
filename: b64DecodeUnicode(data.filemeta[0].filename_text),
full_remote_path: b64DecodeUnicode(data.filemeta[0].full_remote_path_text),
host: data.filemeta[0].host,
size: data.filemeta[0].size,
agent_file_id: data.filemeta[0].agent_file_id,
task_display_id: data.filemeta[0]?.task?.display_id,
complete: data.filemeta[0].complete,
total_chunks: data.filemeta[0].total_chunks,
chunks_received: data.filemeta[0].chunks_received
})
} else {
snackActions.warning("failed to find file specified")
}
},
onError: (data) => {
console.log(data);
const fetchedData = ({data}) => {
if(data.filemeta.length > 0){
setFileMetaData({
id: data.filemeta[0].id,
tags: data.filemeta[0].tags,
filename: b64DecodeUnicode(data.filemeta[0].filename_text),
full_remote_path: b64DecodeUnicode(data.filemeta[0].full_remote_path_text),
host: data.filemeta[0].host,
size: data.filemeta[0].size,
agent_file_id: data.filemeta[0].agent_file_id,
task_display_id: data.filemeta[0]?.task?.display_id,
complete: data.filemeta[0].complete,
total_chunks: data.filemeta[0].total_chunks,
chunks_received: data.filemeta[0].chunks_received
})
} else {
snackActions.warning("failed to find file specified")
}
}
const fetchFileMetaData = useMythicLazyQuery(fileMetaQuery, {
fetchPolicy: "no-cache",
});
React.useEffect( () => {
fetchFileMetaData({variables: {agent_file_id: media?.agent_file_id}}).then( (data) => fetchedData(data)).catch((e) => console.log(e))
}, [task]);
const handleChange = (event, newValue) => {
setValue(newValue);
}
@@ -119,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}
@@ -137,7 +147,8 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
<Tab className={value === 0 ? "selectedCallback": ""} label={"Preview"}></Tab>
<Tab className={value === 1 ? "selectedCallback": ""} label={"Text"}></Tab>
<Tab className={value === 2 ? "selectedCallback": ""} label={"Hex"}></Tab>
<MythicStyledTooltip title={"Download the file"} style={{display: "inline-flex"}}>
<Tab className={value === 3 ? "selectedCallback": ""} label={"Database"}></Tab>
<MythicStyledTooltip title={"Download the file"} tooltipStyle={{display: "inline-flex"}}>
<Button style={{}} size={"small"} href={"/direct/download/" + media.agent_file_id}
download color={"success"}>
<DownloadIcon />
@@ -145,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}
@@ -153,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}
@@ -161,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}
@@ -170,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>
)
}
@@ -215,13 +235,17 @@ export const textExtensionTypesToSyntax = {
"python_history": "python",
}
const knownTextFiles = ["config", "credentials", "known_hosts", "config_default", "id_rsa", "dockerfile", "makefile"];
const imgExtensionTypes = ["png", "jpg", "gif", "jpeg", "pdf"];
const imgExtensionTypes = ["png", "jpg", "gif", "jpeg", "svg"];
const renderExtensionTypes = ["pdf"];
const mimeType = (path) => {
if(!path){return undefined}
let extension = path.split(".");
if(extension.length > 1){
extension = extension[extension.length - 1];
if(imgExtensionTypes.includes(extension.toLowerCase())){
return "image";
}
if(renderExtensionTypes.includes(extension.toLowerCase())){
return "object";
}
if(textExtensionTypes.includes(extension.toLowerCase())){
@@ -234,7 +258,8 @@ const mimeType = (path) => {
return undefined;
}
export const DisplayMedia = ({agent_file_id, filename, expand, task, fileMetaData}) => {
const showMediaSetting = useMythicSetting({setting_name: "showMedia", default_value: "true"});
const showMediaSetting = GetMythicSetting({setting_name: "showMedia", default_value: true});
const [openScreenshot, setOpenScreenshot] = React.useState(false);
const [showMedia, setShowMedia] = React.useState(showMediaSetting);
const [fileData, setFileData] = React.useState({
display: false,
@@ -274,6 +299,11 @@ export const DisplayMedia = ({agent_file_id, filename, expand, task, fileMetaDat
React.useLayoutEffect( () => {
scrollContent()
}, []);
const clickedScreenshot = (e) => {
e.preventDefault();
e.stopPropagation();
setOpenScreenshot(true);
}
if(!showMedia){
return (
<>
@@ -310,16 +340,38 @@ export const DisplayMedia = ({agent_file_id, filename, expand, task, fileMetaDat
</object>
)
}
if(fileData.display_type === "text"){
if(fileData.display_type === "image"){
return (
<div style={{height: "100%", minHeight: "100px", width: "100%"}} >
<DisplayText agent_file_id={agent_file_id} expand={expand} filename={filename} fileMetaData={fileMetaData} />
<>
<img width={"100%"} onClick={clickedScreenshot}
src={"/direct/view/" + agent_file_id} style={{cursor: "zoom-in", width: "100%"}}>
</img>
{openScreenshot &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openScreenshot}
onClose={() => {
setOpenScreenshot(false);
}}
innerDialog={<ResponseDisplayScreenshotModal images={[agent_file_id]}
onClose={() => {
setOpenScreenshot(false);
}}/>}
/>
}
</>
)
}
if (fileData.display_type === "text") {
return (
<div style={{height: "100%", minHeight: "100px", width: "100%"}}>
<DisplayText agent_file_id={agent_file_id} expand={expand} filename={filename}
fileMetaData={fileMetaData}/>
</div>
)
}
return null;
}
const MaxRenderSize = 2000000;
const MaxRenderSize = 2000000; // 2MB
const DisplayFileMetaData = ({fileMetaData}) => {
return (
<Table style={{marginLeft: "0px", width: "100%", tableLayout: "fixed"}}>
@@ -379,12 +431,15 @@ const DisplayText = ({agent_file_id, expand, filename, preview, fileMetaData}) =
const [previewFileString] = useMutation(previewFileQuery, {
onCompleted: (data) => {
if(data.previewFile.status === "success"){
setContent(atob(data.previewFile.contents));
setContent(b64DecodeUnicode(data.previewFile.contents));
if(data.previewFile.size > 512000){
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) => {
@@ -444,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);
}
@@ -600,4 +655,255 @@ const DisplayHex = ({agent_file_id, expand, fileMetaData}) => {
</div>
</div>
);
}
const MAX_ROWS = 1000;
const DisplayDatabase = ({agent_file_id, expand, fileMetaData}) => {
const theme = useTheme();
const queryCountRef = React.useRef(1);
const currentContentRef = React.useRef();
const [selectedTab, setSelectedTab] = React.useState(0);
const [sql, setSql] = React.useState(null);
const [query, setQuery] = React.useState("select * from cookies;");
const [results, setResults] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const onChangeText = (data) => {
setQuery(data);
}
const removeQuery = (event, index) => {
event.stopPropagation();
event.preventDefault();
if(selectedTab === index){
if(index !== 0){
setSelectedTab(index-1);
}
}
setResults(results.toSpliced(index, 1));
}
const onSubmitQuery = (event) => {
if(event){
event.stopPropagation();
event.preventDefault();
}
if(sql){
try{
const result = sql.exec(query);
//console.log(result);
if(result.length > 0){
setResults([...results, {name: `Query: ${queryCountRef.current}`, query: query, results: result[0]}]);
setSelectedTab(queryCountRef.current - 1);
queryCountRef.current += 1;
}
}catch(e){
console.log(e);
snackActions.error(e.message);
}
} else {
console.log(sql);
snackActions.error("Failed to load as SQL Database");
}
}
const onChangeTab = (_, v) => {
const tabQuery = results[v]?.query;
setQuery(tabQuery);
setSelectedTab(v);
}
const onCopyToClipboard = (data) => {
let result = copyStringToClipboard(data);
if(result){
snackActions.success("Copied text!");
}else{
snackActions.error("Failed to copy text");
}
}
const onSaveOutputCSV = (result) => {
const filteredData = result.results.values;
let outputHeaders = "";
let outputRow = "";
outputHeaders = result.results.columns.join(",");
for(let i = 0; i < filteredData.length; i++){
outputRow += filteredData[i].join(",") + "\n"
}
onCopyToClipboard(outputHeaders + "\n" + outputRow);
}
const onSaveOutputPrettyPrint = (result) => {
let tableData = result.results.values;
if(tableData.length === 0){return}
let outputHeaders = result.results.columns;
let outputHeadersMaxLength = outputHeaders.reduce( (prev, cur) => {
return [...prev, cur.length]
}, []);
tableData.map( cur => {
for(let i = 0; i < outputHeaders.length; i++){
if(outputHeadersMaxLength[i] < String(cur[i]).length){
outputHeadersMaxLength[i] = String(cur[i]).length;
}
}
});
let allOutput = "";
for(let i = 0; i < outputHeaders.length; i++){
let remainingSpace = Math.max(0, outputHeadersMaxLength[i] - outputHeaders[i].length);
allOutput += " " + outputHeaders[i].toUpperCase() + " ".repeat(remainingSpace + 2);
}
allOutput += "\n";
for(let i = 0; i < outputHeaders.length; i++){
let remainingSpace = Math.max(0, outputHeadersMaxLength[i] - outputHeaders[i].length);
allOutput += " " + "-".repeat(outputHeaders[i].length) + " ".repeat(remainingSpace + 2);
}
allOutput += "\n";
for(let i = 0; i < tableData.length; i++){
for(let j = 0; j < outputHeaders.length; j++){
let remainingSpace = Math.max(0, outputHeadersMaxLength[j] - String(tableData[i][j]).length);
allOutput += "| " + String(tableData[i][j]) + " ".repeat(remainingSpace) + " ";
}
allOutput += " |\n"
}
onCopyToClipboard(allOutput);
}
React.useEffect( () => {
async function initialize(){
const newSQL = await initSQLJS({locateFile: () => sqlWasm});
fetch('/direct/view/' + agent_file_id).then((response) => {
if(response.status !== 200){
setLoading(false);
snackActions.warning("Failed to fetch contents from Mythic");
return;
}
response.arrayBuffer().then( data => {
const SQL = new newSQL.Database(new Uint8Array(data));
if(SQL){
setSql(SQL);
}
setLoading(false);
}).catch(error => {
setLoading(false);
snackActions.warning("Error getting contents from server: " + error.toString());
console.log("Error trying to get json response", error, response);
});
}).catch(error => {
if(error.toString() === "TypeError: Failed to fetch"){
snackActions.warning("Please refresh and accept the SSL connection error");
} else {
snackActions.warning("Error talking to server: " + error.toString());
}
setLoading(false);
console.log("There was an error!", error);
});
}
initialize();
}, []);
React.useEffect(() => {
const handleKeyDown = (e) => {
// Execute query on Ctrl+Enter or Cmd+Enter when in the SQL editor
if (e.shiftKey && e.key === 'Enter') {
onSubmitQuery(e);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onSubmitQuery]);
return (
<div style={{display: "flex", height: "100%", flexDirection: "column", width: "100%", position: "relative"}}>
<Backdrop open={loading} onClick={()=>{setLoading(false);}} style={{zIndex: 2, position: "absolute"}}>
<CircularProgress color="inherit" disableShrink />
</Backdrop>
<div style={{display: "inline-flex", flexDirection: "row", alignItems: "center", borderBottom: "1px solid grey"}}>
<AceEditor
className={"roundedBottomCorners"}
ref={currentContentRef}
mode={'sql'}
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
fontSize={14}
showGutter={ true}
onChange={onChangeText}
highlightActiveLine={false}
showPrintMargin={false}
value={query}
height={"100%"}
maxLines={3}
width={"100%"}
wrapEnabled={true}
minLines={2}
setOptions={{
showLineNumbers: true,
tabSize: 4,
useWorker: false,
showInvisibles: false,
}}/>
<Button color={"success"} variant={"contained"} onClick={onSubmitQuery} >Query</Button>
</div>
<TabContext style={{width: "100%", height: "100%", borderBottom: "1px solid grey", overflow: "auto"}} value={selectedTab}>
<TabList onChange={onChangeTab} indicatorColor='secondary'
textColor='primary'
sx={{
'& .Mui-selected': {
color: "unset !important"
}
}}>
{results.map((result, index) => (
<Tab key={result.query + index} style={{padding: 0, paddingLeft: "10px", paddingRight: "10px"}} label={
<div style={{display: "inline-block"}}>
{result.name}
<IconButton style={{padding: 0, marginLeft: "5px", marginRight: "5px", marginBottom: "5px"}}
color={"error"}
onClick={(e) => removeQuery(e, index)}>
x
</IconButton>
</div>} value={index} className={selectedTab === index ? "selectedCallback" : ""} />
))}
</TabList>
{results.map((result, index) => (
<div key={index} style={{display: selectedTab === index ? "flex" : "none",
width: "100%", height: "100%", overflow: "auto",
flexDirection: "column"}}>
{result?.results?.values?.length >= MAX_ROWS &&
<Paper key={"overflow" + index} style={{display: selectedTab === index ? "flex" : "none",
justifyContent: "center", hidden: selectedTab !== index}}>
<WarningOutlinedIcon color={"warning"}></WarningOutlinedIcon>
{"Output is truncated to first " + MAX_ROWS + " rows"}
<WarningOutlinedIcon color={"warning"}></WarningOutlinedIcon>
</Paper>
}
<div key={"output" + index} style={{display: selectedTab === index ? "flex" : "none", alignItems: "center", width: "100%"}}>
{"Save Output As: "}
<Button variant={"outlined"} style={{marginLeft: "10px"}} onClick={() => onSaveOutputCSV(result)} >CSV</Button>
<Button variant={"outlined"} style={{marginLeft: "10px"}} onClick={() => onSaveOutputPrettyPrint(result)} >Pretty Print</Button>
</div>
<TabPanel value={index} key={"tabpanel" + index}
style={{padding: 0, height: "100%", width: "100%", overflow: "auto", position: "relative",
display: selectedTab === index ? "flex" : "none",
flexDirection: "column"}} >
<DisplayDatabaseResult result={result?.results} />
</TabPanel>
</div>
))}
</TabContext>
</div>
)
}
const DisplayDatabaseResult = ({result}) => {
const [columns, setColumns] = React.useState(result.columns);
const [rows, setRows] = React.useState(result.values?.slice(0, MAX_ROWS));
return (
<Table stickyHeader style={{width: "100%", height: "100%", position: "absolute"}}>
<TableHead >
<TableRow>
{columns.map((column, index) => (
<TableCell key={"header" + index}>{column}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{rows.map((row, index1) => (
<TableRow key={"row" + index1} hover>
{columns.map((_, index2) => (
<TableCell key={"row" + index1+ "cell" + index2}>
{row[index2]}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
)
}
@@ -1,26 +1,14 @@
import React, { useEffect } from 'react';
import AceEditor from 'react-ace';
import 'ace-builds/src-noconflict/mode-json';
import 'ace-builds/src-noconflict/theme-monokai';
import 'ace-builds/src-noconflict/theme-xcode';
import "ace-builds/src-noconflict/ext-searchbox";
import {useTheme} from '@mui/material/styles';
import {snackActions} from "../../utilities/Snackbar";
import {modeOptions} from "../Search/PreviewFileStringDialog";
import {modeOptions} from "./ResponseDisplayMedia";
import FormControl from '@mui/material/FormControl';
import WrapTextIcon from '@mui/icons-material/WrapText';
import 'ace-builds/src-noconflict/mode-csharp';
import 'ace-builds/src-noconflict/mode-golang';
import 'ace-builds/src-noconflict/mode-html';
import 'ace-builds/src-noconflict/mode-markdown';
import 'ace-builds/src-noconflict/mode-ruby';
import 'ace-builds/src-noconflict/mode-python';
import 'ace-builds/src-noconflict/mode-java';
import 'ace-builds/src-noconflict/mode-javascript';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import MenuItem from '@mui/material/MenuItem';
import TextField from '@mui/material/TextField';
import { IconButton } from '@mui/material';
import { IconButton, Select } from '@mui/material';
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
import { useReactiveVar } from '@apollo/client';
@@ -28,6 +16,7 @@ import { meState } from '../../../cache';
import CodeIcon from '@mui/icons-material/Code';
import {GetOutputFormatAll} from "./ResponseDisplayInteractive";
import PaletteIcon from '@mui/icons-material/Palette';
import Input from '@mui/material/Input';
const MaxRenderSize = 2000000;
export const ResponseDisplayPlaintext = (props) =>{
@@ -44,6 +33,7 @@ export const ResponseDisplayPlaintext = (props) =>{
if(props.onChangeContent){
props?.onChangeContent(data);
}
setPlaintextView(data);
}
useEffect( () => {
if(props.plaintext.length > MaxRenderSize){
@@ -51,9 +41,13 @@ export const ResponseDisplayPlaintext = (props) =>{
setPlaintextView(props.plaintext.substring(0, MaxRenderSize));
} else {
try{
const newPlaintext = JSON.stringify(JSON.parse(String(props.plaintext)), null, 4);
setPlaintextView(newPlaintext);
setMode("json");
if(props.autoFormat === undefined || props.autoFormat){
const newPlaintext = JSON.stringify(JSON.parse(String(props.plaintext)), null, 4);
setPlaintextView(newPlaintext);
setMode("json");
} else {
setPlaintextView(String(props.plaintext));
}
}catch(error){
setPlaintextView(String(props.plaintext));
}
@@ -97,17 +91,31 @@ export const ResponseDisplayPlaintext = (props) =>{
React.useLayoutEffect( () => {
scrollContent()
}, []);
React.useEffect( () => {
setMode(props?.initial_mode || "html");
}, [props?.initial_mode]);
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}
>
@@ -137,19 +145,33 @@ export const ResponseDisplayPlaintext = (props) =>{
</MythicStyledTooltip>
</div>
}
<div style={{height: "1px", width: "100%", display: "flex", zIndex: 1, backgroundColor: theme.palette.secondary.main}}>
{showOptions &&
<UnfoldLessIcon onClick={onChangeShowOptions} style={{cursor: "pointer", position: "relative", top: "-9px"}} />
}
{!showOptions &&
<UnfoldMoreIcon onClick={onChangeShowOptions} style={{cursor: "pointer", position: "relative", top: "-9px"}} />
}
</div>
{props.displayType !== 'console' &&
<div style={{
height: "1px",
width: "100%",
display: "flex",
zIndex: 1,
}}>
{showOptions &&
<UnfoldLessIcon onClick={onChangeShowOptions}
style={{cursor: "pointer", position: "relative", top: "-9px"}}/>
}
{!showOptions &&
<UnfoldMoreIcon onClick={onChangeShowOptions}
style={{cursor: "pointer", position: "relative", top: "-9px"}}/>
}
</div>
}
<div style={{flexGrow: 1, height: "100%", paddingLeft: renderColors ? "15px" : "0px", paddingRight: renderColors ? "5px": "0px"}}>
<div style={{
flexGrow: 1,
height: "100%",
paddingLeft: renderColors ? "15px" : "0px",
paddingRight: renderColors ? "5px" : "0px"
}}>
{renderColors &&
<GetOutputFormatAll data={[
{response: plaintextView, id: props?.task?.id || 0, timestamp: "1970-01-01"}]}
{response: plaintextView, id: props?.task?.id || 0, timestamp: "1970-01-01"}]}
myTask={false}
taskID={props?.task?.id || 0}
useASNIColor={true}
@@ -162,10 +184,10 @@ export const ResponseDisplayPlaintext = (props) =>{
<AceEditor
className={"roundedBottomCorners"}
ref={currentContentRef}
mode={mode}
mode={props.displayType !== 'console' ? mode : 'html'}
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
fontSize={14}
showGutter={true}
showGutter={ props.displayType !== 'console'}
onChange={onChangeText}
//onLoad={onLoad}
highlightActiveLine={false}
@@ -174,14 +196,15 @@ export const ResponseDisplayPlaintext = (props) =>{
height={props.expand ? "100%": undefined}
maxLines={props.expand ? undefined : 20}
width={"100%"}
//style={{backgroundColor: "transparent"}}
//autoScrollEditorIntoView={true}
wrapEnabled={wrapText}
wrapEnabled={props.displayType !== 'console' ? wrapText : true}
minLines={1}
//maxLines={props.expand ? 50 : 20}
setOptions={{
showLineNumbers: true,
showLineNumbers: props.displayType !== 'console',
tabSize: 4,
useWorker: false
useWorker: false,
showInvisibles: false,
}}/>
}
@@ -7,12 +7,10 @@ import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
export const ResponseDisplayScreenshot = (props) =>{
const [openScreenshot, setOpenScreenshot] = React.useState(false);
const now = (new Date()).toUTCString();
const clickOpenScreenshot = () => {
setOpenScreenshot(true);
}
const scrollContent = (node, isAppearing) => {
const scrollContent = (node, isAppearing) => {
// only auto-scroll if you issued the task
document.getElementById(`scrolltotaskbottom${props.task.id}`)?.scrollIntoView({
//behavior: "smooth",
@@ -20,21 +18,21 @@ export const ResponseDisplayScreenshot = (props) =>{
inline: "nearest"
})
}
React.useLayoutEffect( () => {
React.useLayoutEffect( () => {
scrollContent()
}, []);
return (
<>
{openScreenshot &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openScreenshot}
onClose={()=>{setOpenScreenshot(false);}}
innerDialog={<ResponseDisplayScreenshotModal images={props.agent_file_id} onClose={()=>{setOpenScreenshot(false);}} />}
/>
<MythicDialog fullWidth={true} maxWidth="xl" open={openScreenshot}
onClose={()=>{setOpenScreenshot(false);}}
innerDialog={<ResponseDisplayScreenshotModal images={[props.agent_file_id]} onClose={()=>{setOpenScreenshot(false);}} />}
/>
}
<pre style={{display: "inline-block"}}>
{props?.plaintext || ""}
</pre>
<MythicStyledTooltip title={props?.hoverText || "View Screenshot"} >
<MythicStyledTooltip title={props?.hoverText || "View Screenshot (s)"} >
<Button color="primary" variant={props.variant ? props.variant : "contained"} onClick={clickOpenScreenshot} style={{marginBottom: "10px"}}>{props.name}</Button>
</MythicStyledTooltip>
</>
@@ -1,6 +1,6 @@
import React, {useEffect, useRef} from 'react';
import {Button} from '@mui/material';
import { MythicViewJSONAsTableDialog, MythicDialog } from '../../MythicComponents/MythicDialog';
import {MythicViewJSONAsTableDialog, MythicDialog, MythicModifyStringDialog} from '../../MythicComponents/MythicDialog';
import { MythicDisplayTextDialog } from '../../MythicComponents/MythicDisplayTextDialog';
import { ResponseDisplayTableDialogTable } from './ResponseDisplayTableDialogTable';
import Paper from '@mui/material/Paper';
@@ -15,8 +15,13 @@ import {snackActions} from '../../utilities/Snackbar';
import {MythicStyledTooltip} from '../../MythicComponents/MythicStyledTooltip';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import MythicResizableGrid from '../../MythicComponents/MythicResizableGrid';
import {faList, faTrashAlt, faSkullCrossbones, faCamera, faSyringe, faFolder, faFolderOpen, faFileArchive, faCog, faFileWord, faFileExcel, faFilePowerpoint, faFilePdf, faDatabase, faKey, faFileCode, faDownload, faUpload, faFileImage, faCopy, faBoxOpen, faFileAlt } from '@fortawesome/free-solid-svg-icons';
import {faList, faTrashAlt, faSkullCrossbones, faCamera, faSyringe, faFolder, faFolderOpen, faFileArchive, faCog,
faFileWord, faFileExcel, faFilePowerpoint, faFilePdf, faDatabase, faKey, faFileCode, faDownload, faUpload,
faFileImage, faCopy, faBoxOpen, faFileAlt, faCirclePlus, faCheck, faSquareXmark, faRotate } from '@fortawesome/free-solid-svg-icons';
import {Dropdown, DropdownMenuItem} from "../../MythicComponents/MythicNestedMenus";
import {GetComputedFontSize} from "../../MythicComponents/MythicSavedUserSetting";
import {TableFilterDialog} from "./TableFilterDialog";
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
const onCopyToClipboard = (data) => {
let result = copyStringToClipboard(data);
@@ -28,6 +33,14 @@ const onCopyToClipboard = (data) => {
}
export const getIconName = (iconName) => {
switch(iconName.toLowerCase()){
case "add":
return faCirclePlus;
case "x":
return faSquareXmark;
case "check":
return faCheck;
case "refresh":
return faRotate;
case "openfolder":
case "folder":
return faFolderOpen;
@@ -39,6 +52,7 @@ export const getIconName = (iconName) => {
case "diskimage":
return faBoxOpen;
case "executable":
case "cog":
return faCog;
case "word":
return faFileWord;
@@ -75,28 +89,63 @@ export const getIconName = (iconName) => {
case "camera":
return faCamera;
default:
return faFileAlt;
return iconName;
}
}
export const getIconColor = (theme, color) => {
switch(color){
case "info":
return theme.palette.info.main;
case "warning":
return theme.palette.warning.main;
case "primary":
return theme.palette.primary.main;
case "error":
return theme.palette.error.main;
case "success":
return theme.palette.success.main;
case "secondary":
return theme.palette.success.main;
default:
return color;
}
}
const doubleClickRow = () => {
}
const ResponseDisplayTableStringCellCopy = ({cellData}) => {
return (
cellData?.copyIcon &&
<MythicStyledTooltip title={"Copy to clipboard"}>
<IconButton onClick={() => onCopyToClipboard(cellData["plaintext"])} size="small">
<FontAwesomeIcon icon={faCopy} />
</IconButton>
</MythicStyledTooltip>
)
}
const ResponseDisplayTableFontAwesomeStartIcon = ({cellData}) => {
const theme = useTheme();
return (
cellData?.startIcon &&
<MythicStyledTooltip title={cellData?.startIconHoverText || ""} >
<FontAwesomeIcon icon={getIconName(cellData?.startIcon)} style={{marginRight: "5px", color: getIconColor(theme, cellData?.startIconColor || "")}}/>
</MythicStyledTooltip>
)
}
const ResponseDisplayTableFontAwesomeEndIcon = ({cellData}) => {
const theme = useTheme();
return (
cellData?.endIcon &&
<MythicStyledTooltip title={cellData?.endIconHoverText || ""}>
<FontAwesomeIcon icon={getIconName(cellData?.endIcon)} style={{color: getIconColor(theme, cellData?.endIconColor || "")}}/>
</MythicStyledTooltip>
)
}
const ResponseDisplayTableStringCell = ({cellData, rowData}) => {
return (
<div style={{...(cellData?.cellStyle || null), height: "100%"}}>
{cellData?.copyIcon?
<MythicStyledTooltip title={"Copy to clipboard"}>
<IconButton onClick={() => onCopyToClipboard(cellData["plaintext"])} size="small">
<FontAwesomeIcon icon={faCopy} />
</IconButton>
</MythicStyledTooltip> : null}
{cellData?.startIcon?
<MythicStyledTooltip title={cellData?.startIconHoverText || ""} >
<FontAwesomeIcon icon={getIconName(cellData?.startIcon)} style={{marginRight: "5px", color: cellData?.startIconColor || ""}}/>
</MythicStyledTooltip>
: null
}
<ResponseDisplayTableStringCellCopy cellData={cellData} />
<ResponseDisplayTableFontAwesomeStartIcon cellData={cellData} />
{cellData?.plaintextHoverText? (
<MythicStyledTooltip title={cellData.plaintextHoverText}>
<pre style={{display: "inline-block", margin: 0}}>
@@ -109,11 +158,7 @@ const ResponseDisplayTableStringCell = ({cellData, rowData}) => {
{cellData?.plaintext?.replaceAll?.("\n","") || " "}
</pre>
)}
{cellData?.endIcon?
<MythicStyledTooltip title={cellData?.endIconHoverText || ""}>
<FontAwesomeIcon icon={getIconName(cellData?.endIcon)} style={{color: cellData?.endIconColor || ""}}/>
</MythicStyledTooltip>: null
}
<ResponseDisplayTableFontAwesomeEndIcon cellData={cellData} />
</div>
);
}
@@ -194,6 +239,14 @@ const ResponseDisplayTableSizeCell = ({cellData, rowData}) => {
}
const actionCellButtonStyle = {paddingTop: 0, paddingBottom: 0};
const ResponseDisplayTableActionCell = ({cellData, callback_id, rowData}) => {
return (
<div style={{ height: "100%"}}>
{cellData?.plaintext && cellData.plaintext}
{cellData?.button && <ResponseDisplayTableActionCellButton cellData={cellData} callback_id={callback_id} />}
</div>
);
}
const ResponseDisplayTableActionCellButton = ({cellData, callback_id}) => {
const theme = useTheme();
const [openButton, setOpenButton] = React.useState(false);
const [openTaskingButton, setOpenTaskingButton] = React.useState(false);
@@ -246,168 +299,207 @@ const ResponseDisplayTableActionCell = ({cellData, callback_id, rowData}) => {
setOpenTableButton(false);
setTaskingData({});
}
const getButtonObject = () => {
switch(cellData.button.type.toLowerCase()){
switch(cellData.button.type.toLowerCase()){
case "dictionary":
return (
<React.Fragment>
<MythicStyledTooltip title={cellData?.button?.hoverText || "View Data"} >
<Button size="small" color="primary"
onClick={() => setOpenButton(true)} disabled={cellData?.button?.disabled || false}
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.startIconColor}}/> : null}
style={{...actionCellButtonStyle}}
>{cellData?.button?.name}</Button>
</MythicStyledTooltip>
{openButton &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openButton}
onClose={()=>{setOpenButton(false);}}
innerDialog={<MythicViewJSONAsTableDialog title={cellData?.button?.title} leftColumn={cellData?.button?.leftColumnTitle}
rightColumn={cellData?.button?.rightColumnTitle} value={cellData?.button?.value || {}} onClose={()=>{setOpenButton(false);}} />}
/>
<React.Fragment>
<MythicStyledTooltip title={cellData?.button?.hoverText || "View Data"} >
<Button size="small" color="info"
onClick={() => setOpenButton(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>
</MythicStyledTooltip>
{openButton &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openButton}
onClose={()=>{setOpenButton(false);}}
innerDialog={<MythicViewJSONAsTableDialog title={cellData?.button?.title} leftColumn={cellData?.button?.leftColumnTitle}
rightColumn={cellData?.button?.rightColumnTitle} value={cellData?.button?.value || {}} onClose={()=>{setOpenButton(false);}} />}
/>
}
</React.Fragment>
)
case "string":
return (
<React.Fragment>
<MythicStyledTooltip title={cellData?.button?.hoverText || "View Data"} >
<Button size="small" color="primary"
onClick={() => setOpenButton(true)} disabled={cellData?.button?.disabled || false}
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.startIconColor}}/> : null}
style={{...actionCellButtonStyle}}
>{cellData?.button?.name}</Button>
</MythicStyledTooltip>
{openButton &&
<MythicDisplayTextDialog fullWidth={true} maxWidth="lg" open={openButton} title={cellData?.button?.title} value={cellData?.button?.value || ""}
onClose={()=>{setOpenButton(false);}}
/>
<React.Fragment>
<MythicStyledTooltip title={cellData?.button?.hoverText || "View Data"} >
<Button size="small" color="info"
onClick={() => setOpenButton(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>
</MythicStyledTooltip>
{openButton &&
<MythicDisplayTextDialog fullWidth={true} maxWidth="lg" open={openButton} title={cellData?.button?.title} value={cellData?.button?.value || ""}
onClose={()=>{setOpenButton(false);}}
/>
}
</React.Fragment>
</React.Fragment>
)
case "table":
case "table":
return (
<React.Fragment>
<MythicStyledTooltip title={cellData?.button?.hoverText || "View Data"} >
<Button size="small" color="primary"
onClick={() => setOpenButton(true)} disabled={cellData?.button?.disabled || false}
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.startIconColor || ""}}/> : null}
style={{...actionCellButtonStyle}}
>{cellData?.button?.name}</Button>
</MythicStyledTooltip>
{openButton &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openButton}
onClose={()=>{setOpenButton(false);}}
innerDialog={<ResponseDisplayTableDialogTable title={cellData?.button?.title}
table={cellData?.button?.value || {}} callback_id={callback_id} onClose={()=>{setOpenButton(false);}} />}
/>
<React.Fragment>
<MythicStyledTooltip title={cellData?.button?.hoverText || "View Data"} >
<Button size="small" color="info"
onClick={() => setOpenButton(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>
</MythicStyledTooltip>
{openButton &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openButton}
onClose={()=>{setOpenButton(false);}}
innerDialog={<ResponseDisplayTableDialogTable title={cellData?.button?.title}
table={cellData?.button?.value || {}} callback_id={callback_id} onClose={()=>{setOpenButton(false);}} />}
/>
}
</React.Fragment>
</React.Fragment>
)
case "task":
return (
<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?.startIconColor || ""}}/> : null}
style={{...actionCellButtonStyle}}
>{cellData?.button?.name ? cellData?.button?.name : cellData?.button?.startIcon ? null : "Submit Task"}</Button>
</MythicStyledTooltip>
{openTaskingButton &&
<TaskFromUIButton ui_feature={cellData?.button?.ui_feature || " "}
callback_id={callback_id}
parameters={cellData?.button?.parameters || ""}
openDialog={cellData?.button?.openDialog || false}
getConfirmation={cellData?.button?.getConfirmation || false}
acceptText={cellData?.button?.acceptText || "confirm"}
selectCallback={cellData?.button?.selectCallback || false}
onTasked={() => setOpenTaskingButton(false)}/>
}
</React.Fragment>
<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 || "")}}/> : undefined}
style={{...actionCellButtonStyle}}
>{cellData?.button?.name ? cellData?.button?.name : cellData?.button?.startIcon ? undefined : "Submit Task"}</Button>
</MythicStyledTooltip>
{openTaskingButton &&
<TaskFromUIButton ui_feature={cellData?.button?.ui_feature || " "}
callback_id={callback_id}
parameters={cellData?.button?.parameters || ""}
openDialog={cellData?.button?.openDialog || false}
getConfirmation={cellData?.button?.getConfirmation || false}
acceptText={cellData?.button?.acceptText || "confirm"}
selectCallback={cellData?.button?.selectCallback || false}
onTasked={() => setOpenTaskingButton(false)}/>
}
</React.Fragment>
)
case "menu":
return (
<React.Fragment>
{openTaskingButton &&
<TaskFromUIButton ui_feature={taskingData.ui_feature}
callback_id={callback_id}
parameters={taskingData.parameters}
openDialog={taskingData?.openDialog || false}
getConfirmation={taskingData?.getConfirmation || false}
acceptText={taskingData?.acceptText || "confirm"}
selectCallback={taskingData?.selectCallback || false}
onTasked={finishedTasking}/>
}
{openDictionaryButton &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openDictionaryButton}
onClose={finishedViewingData}
innerDialog={<MythicViewJSONAsTableDialog title={taskingData.title} leftColumn={taskingData.leftColumnTitle}
rightColumn={taskingData.rightColumnTitle} value={taskingData.value} onClose={finishedViewingData} />}
/>
}
{openStringButton &&
<MythicDisplayTextDialog fullWidth={true} maxWidth="lg" open={openStringButton} title={taskingData?.title} value={taskingData?.value || ""}
onClose={finishedViewingData}
/>
}
{openTableButton &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openTableButton}
onClose={finishedViewingData}
innerDialog={<ResponseDisplayTableDialogTable title={taskingData?.title}
table={taskingData?.value || {}} callback_id={callback_id} onClose={finishedViewingData} />}
/>
<React.Fragment>
{openTaskingButton &&
<TaskFromUIButton ui_feature={taskingData.ui_feature}
callback_id={callback_id}
parameters={taskingData.parameters}
openDialog={taskingData?.openDialog || false}
getConfirmation={taskingData?.getConfirmation || false}
acceptText={taskingData?.acceptText || "confirm"}
selectCallback={taskingData?.selectCallback || false}
onTasked={finishedTasking}/>
}
<Button size="small" color="primary" ref={dropdownAnchorRef}
onClick={() => setOpenDropdownButton(true)} disabled={cellData?.button?.disabled || false}
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.startIconColor || ""}}/> : null}
style={{...actionCellButtonStyle}}
>{cellData?.button?.name || " "}</Button>
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
<Dropdown
isOpen={dropdownAnchorRef.current}
onOpen={setOpenDropdownButton}
externallyOpen={openDropdownButton}
menu={
cellData.button.value.map((option, index) => (
<DropdownMenuItem
key={option.name + index}
disabled={option.disabled}
onClick={(event) => handleMenuItemClick(event, index)}
>
<MythicStyledTooltip title={option?.hoverText || (option.type === "task" ? "Task an Agent" : "Display Data")}>
{option?.startIcon ? <FontAwesomeIcon icon={getIconName(option?.startIcon)} style={{color: option?.startIconColor || "", marginRight: "5px"}}/> : null}
{option.name}
</MythicStyledTooltip>
</DropdownMenuItem>
))
}
/>
</ClickAwayListener>
</React.Fragment>
{openDictionaryButton &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openDictionaryButton}
onClose={finishedViewingData}
innerDialog={<MythicViewJSONAsTableDialog title={taskingData.title} leftColumn={taskingData.leftColumnTitle}
rightColumn={taskingData.rightColumnTitle} value={taskingData.value} onClose={finishedViewingData} />}
/>
}
{openStringButton &&
<MythicDisplayTextDialog fullWidth={true} maxWidth="lg" open={openStringButton} title={taskingData?.title} value={taskingData?.value || ""}
onClose={finishedViewingData}
/>
}
{openTableButton &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openTableButton}
onClose={finishedViewingData}
innerDialog={<ResponseDisplayTableDialogTable title={taskingData?.title}
table={taskingData?.value || {}} callback_id={callback_id} onClose={finishedViewingData} />}
/>
}
<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, color: theme.palette.text.primary}}
>{cellData?.button?.name || " "} <ArrowDropDownIcon />
</Button>
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
<Dropdown
isOpen={dropdownAnchorRef.current}
onOpen={setOpenDropdownButton}
externallyOpen={openDropdownButton}
menu={
cellData.button.value.map((option, index) => (
<DropdownMenuItem
key={option.name + index}
disabled={option.disabled}
onClick={(event) => handleMenuItemClick(event, index)}
>
<MythicStyledTooltip title={option?.hoverText || (option.type === "task" ? "Task an Agent" : "Display Data")}>
{option?.startIcon ? <FontAwesomeIcon icon={getIconName(option?.startIcon)} style={{color: getIconColor(theme, cellData?.button?.startIconColor || ""), marginRight: "5px"}}/> : null}
{option.name}
</MythicStyledTooltip>
</DropdownMenuItem>
))
}
/>
</ClickAwayListener>
</React.Fragment>
)
}
}
return (
<div style={{...(rowData?.rowStyle || null), ...(cellData?.cellStyle || null), height: "100%"}}>
{cellData?.plaintext ? cellData.plaintext : null}
{cellData?.button ? (getButtonObject()) : null}
</div>
);
}
const createRowCells = ({row, rowIndex, headers, callback_id}) => {
return headers.map((header, colIndex) => {
const cellData = row[header.plaintext];
const key = `${rowIndex}-${colIndex}-${header.plaintext}`;
switch(header.type){
case "string":
return <ResponseDisplayTableStringCell key={key} cellData={cellData} rowData={row}/>
case "size":
return <ResponseDisplayTableSizeCell key={key} cellData={cellData} rowData={row}/>
case "button":
return <ResponseDisplayTableActionCell callback_id={callback_id} key={key} cellData={cellData} rowData={row}/>
case "number":
return <ResponseDisplayTableNumberCell callback_id={callback_id} key={key} cellData={cellData} rowData={row} />
default:
return <ResponseDisplayTableStringCell key={key} cellData={cellData} rowData={row}/>
}
})
}
export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
const theme = useTheme();
const rowHeight = 20;
const headerHeight = 45;
const rowHeight = GetComputedFontSize() + 7;
const headerHeight = GetComputedFontSize() + 32;
const maxHeight = 375;
const [dataHeight, setDataHeight] = React.useState(maxHeight);
const [allData, setAllData] = React.useState([]);
const [selectedColumn, setSelectedColumn] = React.useState({});
const [filterOptions, setFilterOptions] = React.useState({});
const [openContextMenu, setOpenContextMenu] = React.useState(false);
const columns = React.useMemo(
() =>
table?.headers?.reduce( (prev, cur) => {
if(filterOptions[cur.plaintext] && String(filterOptions[cur.plaintext]).length > 0){
return [...prev, {...cur, filtered: true}];
}else{
return [...prev, {...cur}];
}
}, []) || []
, [filterOptions, table.headers]
);
const [sortData, setSortData] = React.useState({sortKey: null, sortType: null, sortDirection: "ASC"})
const filterRow = (row) => {
for(const [key,value] of Object.entries(filterOptions)){
if(!String(row[key].plaintext).toLowerCase().includes(String(value).toLowerCase())){
return true;
}
}
return false;
}
const sortedData = React.useMemo(() => {
const tmpData = [...allData].reduce( (prev, cur) => {
if(filterRow(cur)){
return [...prev];
}else{
return [...prev, cur];
}
}, []);
if (sortData.sortKey === null || sortData.sortType === null) {
return allData
return tmpData
}
const tmpData = [...allData];
if(sortData.sortType === "number" || sortData.sortType === "size"){
tmpData.sort((a, b) => {
if(a[sortData.sortKey]["plaintext"] === b[sortData.sortKey]["plaintext"]){
@@ -466,7 +558,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
tmpData.reverse();
}
return tmpData;
}, [allData, sortData]);
}, [allData, sortData, filterOptions]);
const onClickHeader = (e, columnIndex) => {
const column = table.headers[columnIndex];
@@ -478,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"});
@@ -488,41 +580,15 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
};
const gridData = React.useMemo(
() => {
return sortedData.map((row) => {
let rowData = [];
for(let i = 0; i < table.headers.length; i++){
switch(table.headers[i].type){
case "string":
rowData.push(
<ResponseDisplayTableStringCell cellData={row[table.headers[i]['plaintext']]} rowData={row}/>
)
break;
case "size":
rowData.push(
<ResponseDisplayTableSizeCell cellData={row[table.headers[i]['plaintext']]} rowData={row}/>
)
break;
case "button":
rowData.push(
<ResponseDisplayTableActionCell callback_id={callback_id} cellData={row[table.headers[i]['plaintext']]} rowData={row}/>
)
break;
case "number":
rowData.push(
<ResponseDisplayTableNumberCell callback_id={callback_id} cellData={row[table.headers[i]["plaintext"]]} rowData={row} />
)
break;
default:
rowData.push(
<ResponseDisplayTableStringCell cellData={row[table.headers[i]['plaintext']]} rowData={row}/>
)
break;
}
}
return [...rowData];
return sortedData.map((row, rowIndex) => {
return createRowCells({row, headers: table.headers, callback_id, rowIndex});
});
}, [sortedData]
}, [sortedData, table?.headers, callback_id]
);
const onSubmitFilterOptions = (value) => {
setFilterOptions({...filterOptions, [selectedColumn.key]: value });
}
const filterOutButtonsFromRowData = (data) => {
let rowData = {};
for(const key of Object.keys(data)){
@@ -532,7 +598,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
}
return rowData;
}
const contextMenuOptions = [
const rowContextMenuOptions = [
{
name: 'Copy Row as JSON', icon: null,
click: ({event, columnIndex, rowIndex, data}) => {
@@ -562,7 +628,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
},
type: "item",
},
{
{
name: 'Copy Row as TSV', icon: null,
click: ({event, columnIndex, rowIndex, data}) => {
const filteredData = filterOutButtonsFromRowData(data);
@@ -584,30 +650,173 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
},
type: "item",
},
{
name: 'Copy Cell', icon: null,
click: ({event, columnIndex, rowIndex, data}) => {
if(table.headers !== undefined && table.headers[columnIndex] !== undefined){
let field = table.headers[columnIndex];
if(field.plaintext !== undefined){ // column name
if(data[field.plaintext].plaintext !== undefined){
onCopyToClipboard(data[field.plaintext].plaintext);
}
}
}
}, type: "item"
},
];
const contextMenuOptions = [
{
name: 'Filter Column', type: "item", icon: null,
click: ({event, columnIndex}) => {
event.preventDefault();
event.stopPropagation();
if(table.headers[columnIndex].disableFilterMenu){
snackActions.warning("Can't filter that column");
return;
}
setSelectedColumn({
"key": table.headers[columnIndex].plaintext,
"name": table.headers[columnIndex].plaintext
});
setOpenContextMenu(true);
}
},
{
name: "Copy Table as JSON", type: "item", icon: null,
click: ({event, columnIndex}) => {
event.preventDefault();
event.stopPropagation();
let tableData = sortedData.map(d => filterOutButtonsFromRowData(d))
onCopyToClipboard(JSON.stringify(tableData, null, 2));
}
},
{
name: "Copy Table as CSV", type: "item", icon: null,
click: ({event, columnIndex}) => {
event.preventDefault();
event.stopPropagation();
let tableData = sortedData.map(d => filterOutButtonsFromRowData(d));
if(tableData.length === 0){return}
let outputHeaders = "";
let allOutput = "";
for(let i = 0; i < tableData.length; i++){
let outputRow = "";
for(const key of Object.keys(tableData[i])){
if(i === 0){
if(outputHeaders === ""){
outputHeaders += key;
} else {
outputHeaders += "," + key;
}
}
if(outputRow === ""){
outputRow += tableData[i][key];
}else{
outputRow += "," + tableData[i][key];
}
}
allOutput += outputRow + "\n";
}
onCopyToClipboard(outputHeaders + "\n" + allOutput);
}
},
{
name: "Copy Table as TSV", type: "item", icon: null,
click: ({event, columnIndex}) => {
event.preventDefault();
event.stopPropagation();
let tableData = sortedData.map(d => filterOutButtonsFromRowData(d));
if(tableData.length === 0){return}
let outputHeaders = "";
let allOutput = "";
for(let i = 0; i < tableData.length; i++){
let outputRow = "";
for(const key of Object.keys(tableData[i])){
if(i === 0){
if(outputHeaders === ""){
outputHeaders += key;
} else {
outputHeaders += "\t" + key;
}
}
if(outputRow === ""){
outputRow += tableData[i][key];
}else{
outputRow += "\t" + tableData[i][key];
}
}
allOutput += outputRow + "\n";
}
onCopyToClipboard(outputHeaders + "\n" + allOutput);
}
},
{
name: "Copy Table as PrettyPrint Text", type: "item", icon: null,
click: ({event, columnIndex}) => {
event.preventDefault();
event.stopPropagation();
let tableData = sortedData.map(d => filterOutButtonsFromRowData(d));
if(tableData.length === 0){return}
let outputHeaders = Object.keys(tableData[0]);
let outputHeadersMaxLength = outputHeaders.reduce( (prev, cur) => {
return {...prev, [cur]: cur.length}
}, {});
tableData.map( cur => {
for(let i = 0; i < outputHeaders.length; i++){
if(outputHeadersMaxLength[outputHeaders[i]] < String(cur[outputHeaders[i]]).length){
outputHeadersMaxLength[outputHeaders[i]] = String(cur[outputHeaders[i]]).length;
}
}
});
let allOutput = "";
for(let i = 0; i < outputHeaders.length; i++){
let remainingSpace = Math.max(0, outputHeadersMaxLength[outputHeaders[i]] - outputHeaders[i].length);
allOutput += " " + outputHeaders[i].toUpperCase() + " ".repeat(remainingSpace + 2);
}
allOutput += "\n";
for(let i = 0; i < outputHeaders.length; i++){
let remainingSpace = Math.max(0, outputHeadersMaxLength[outputHeaders[i]] - outputHeaders[i].length);
allOutput += " " + "-".repeat(outputHeaders[i].length) + " ".repeat(remainingSpace + 2);
}
allOutput += "\n";
for(let i = 0; i < tableData.length; i++){
for(let j = 0; j < outputHeaders.length; j++){
let remainingSpace = Math.max(0, outputHeadersMaxLength[outputHeaders[j]] - String(tableData[i][outputHeaders[j]]).length);
allOutput += "| " + String(tableData[i][outputHeaders[j]]) + " ".repeat(remainingSpace) + " ";
}
allOutput += " |\n"
}
onCopyToClipboard(allOutput);
}
},
];
useEffect( () => {
setAllData([...table.rows]);
setDataHeight(Math.min(maxHeight, (table.rows.length * rowHeight) + headerHeight));
}, [table.rows])
const sortColumn = table.headers.findIndex((column) => column.plaintext === sortData.sortKey);
const tableStyle = React.useMemo( () => {
return expand ? {flexGrow: 1, width: "99%", position: "relative"} : {height: dataHeight, position: "relative"}
}, [expand, dataHeight]);
return expand ? {flexGrow: 1,
minHeight: gridData.length > 0 ? Math.min(maxHeight, dataHeight) : headerHeight,
width: "100%", position: "relative"} :
{height: dataHeight, position: "relative"}
}, [expand, dataHeight, gridData]);
return (
<div style={{height: "100%", display: "flex", flexDirection: "column", position: "relative", width: "100%"}}>
{table?.title ? (
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main, marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
<Typography variant="h5" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
{table?.title && (
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main}} variant={"elevation"}>
<Typography variant="h6" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
{table.title}
</Typography>
</Paper>
) : null}
)}
<div style={tableStyle}>
<MythicResizableGrid
columns={table.headers}
columns={columns}
sortIndicatorIndex={sortColumn}
sortDirection={sortData.sortDirection}
items={gridData}
@@ -616,8 +825,24 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
onDoubleClickRow={doubleClickRow}
rowHeight={rowHeight}
onClickHeader={onClickHeader}
rowContextMenuOptions={contextMenuOptions}
contextMenuOptions={contextMenuOptions}
rowContextMenuOptions={rowContextMenuOptions}
/>
{openContextMenu &&
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
onClose={()=>{setOpenContextMenu(false);}}
innerDialog={
<MythicModifyStringDialog
title='Filter Column'
onSubmit={onSubmitFilterOptions}
value={filterOptions[selectedColumn.key]}
onClose={() => {
setOpenContextMenu(false);
}}
/>
}
/>
}
</div>
</div>
@@ -0,0 +1,108 @@
import React from 'react';
import AppBar from '@mui/material/AppBar';
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
import {a11yProps} from "../../MythicComponents/MythicTabPanel";
import {ResponseDisplayBrowserScriptComponent} from "./ResponseDisplay";
function ResponseDisplayTabsLabel(props) {
const { label, index, ...other } =
props;
return (
<Tab
label={
<span>
{label}
</span>
}
sx={{
'&.Mui-selected': {
border: "2px solid grey",
borderRadius: "4px",
//backgroundColor: 'info.main', // Color when selected
},
}}
wrapped={true}
{...a11yProps(index)}
{...other}
/>
);
}
function ResponseDisplayTabsPanel(props) {
const { children, value, index, ...other } =
props;
const style =
props.style === undefined
? {
display: value === index ? 'flex' : 'none',
flexDirection: 'column',
flexGrow: 1,
width: '100%',
maxWidth: '100%',
overflowY: "auto",
}
: props.style;
return (
<div
role='tabpanel'
hidden={value !== index}
id={`scrollable-auto-tabpanel-${index}`}
aria-labelledby={`scrollable-auto-tab-${index}`}
style={style}
{...other}>
{<React.Fragment>{children}</React.Fragment>}
</div>
);
}
export function ResponseDisplayTabs({ tabs, task, expand, displayType, output }) {
const mountedRef = React.useRef(true);
const [value, setValue] = React.useState(0);
const handleChange = (event, newValue) => {
setValue(newValue);
};
React.useEffect( () => {
return() => {
mountedRef.current = false;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<div style={{width: "100%", maxWidth: "100%", display: 'flex', flexDirection: 'column', flexGrow: 1, height: expand ? "100%" : "400px", }}>
<AppBar color='default' position={"static"} style={{maxHeight: "25%", overflowY: 'auto', maxWidth: "100%", flexWrap: "no-wrap"}} >
<Tabs
value={value}
variant={"standard"}
onChange={handleChange}
indicatorColor='primary'
textColor='primary'
style={{ }}
TabIndicatorProps={{style: {
display: "none",
}}}
aria-label='scrollable tabs'>
{tabs.map((tab, index) => (
<ResponseDisplayTabsLabel
key={'tablabel' + task.id + index}
index={index}
label={tab.title}
/>
))}
</Tabs>
</AppBar>
{tabs.map((tab, index) => (
<ResponseDisplayTabsPanel
key={'tabpanel' + task.id + index}
value={value}
index={index}>
<ResponseDisplayBrowserScriptComponent
task={task} expand={expand} displayType={displayType} output={output}
browserScriptData={tab.content}
/>
</ResponseDisplayTabsPanel>
))}
</div>
);
}
@@ -24,8 +24,9 @@ export function TableFilterDialog({filterOptions, onSubmit, onClose, selectedCol
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Filter {selectedColumn.name} Entries</DialogTitle>
<DialogContent dividers={true}>
<DialogTitle id="form-dialog-title">Filter "{selectedColumn.name}" Entries</DialogTitle>
<DialogContent style={{paddingBottom: 0}} dividers={true}>
Show only rows that include the following case insensitive text:
<MythicTextField autoFocus onChange={onChange} value={description} onEnter={onCommitSubmit}/>
</DialogContent>
<DialogActions>
@@ -1,9 +1,4 @@
import React, {useState} from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import MythicTextField from '../../MythicComponents/MythicTextField';
import {useQuery, gql, useMutation} from '@apollo/client';
import LinearProgress from '@mui/material/LinearProgress';
import {MythicModifyStringDialog} from "../../MythicComponents/MythicDialog";
@@ -43,6 +38,7 @@ export function TaskCommentDialog(props) {
const [updateComment] = useMutation(updateCommentMutation, {
update: (cache, {data}) => {
//console.log(data);
props.onClose();
}
});
if (loading) {
@@ -54,17 +50,14 @@ export function TaskCommentDialog(props) {
}
const onCommitSubmit = (newValue) => {
updateComment({variables: {task_id: props.task_id, comment: newValue}});
props.onClose();
}
const onChange = (name, value, error) => {
setComment(value);
}
return (
<React.Fragment>
<MythicModifyStringDialog title={`Edit Task Comment`}
onClose={props.onClose}
value={comment}
dontCloseOnSubmit={true}
onSubmit={onCommitSubmit} />
</React.Fragment>
);
@@ -3,7 +3,7 @@ import { styled } from '@mui/material/styles';
import {IconButton, Link} from '@mui/material';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import { toLocalTime } from '../../utilities/Time';
import {getSkewedNow, toLocalTime, toLocalTimeShort} from '../../utilities/Time';
import Accordion from '@mui/material/Accordion';
import AccordionSummary from '@mui/material/AccordionSummary';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
@@ -15,10 +15,22 @@ import {gql, useSubscription } from '@apollo/client';
import {TaskDisplayContainer, TaskDisplayContainerConsole} from './TaskDisplayContainer';
import {TagsDisplay} from '../../MythicComponents/MythicTag';
import {taskingDataFragment} from './CallbackMutations';
import {useMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
import {GetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
import PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwoTone';
import CropRotateTwoToneIcon from '@mui/icons-material/CropRotateTwoTone';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {operatorSettingDefaults} from "../../../cache";
import {TaskFromUIButton} from "./TaskFromUIButton";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {faSkullCrossbones} from '@fortawesome/free-solid-svg-icons';
import LockOpenIcon from '@mui/icons-material/LockOpen';
// Icons for console-view display
import AccessTimeIcon from '@mui/icons-material/AccessTime';
import PersonOutlineIcon from '@mui/icons-material/PersonOutline';
import ComputerIcon from '@mui/icons-material/Computer';
import PublicIcon from '@mui/icons-material/Public';
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
import NumbersIcon from '@mui/icons-material/Numbers';
const PREFIX = 'TaskDisplay';
@@ -51,9 +63,9 @@ export const StyledPaper = styled(Paper)((
marginTop: "3px",
marginRight: "0px",
height: "auto",
width: "99%",
width: "100%",
boxShadow: "unset",
backgroundColor: theme.palette.background.taskLabel,
backgroundColor: theme.palette.background.default + "CC",
},
[`& .${classes.heading}`]: {
@@ -65,10 +77,11 @@ export const StyledPaper = styled(Paper)((
WebkitBoxOrient: "vertical",
cursor: "default",
wordBreak: "break-all",
//color: theme.taskPromptTextColor,
},
[`& .${classes.secondaryHeading}`]: {
fontSize: theme.typography.pxToRem(15),
//color: theme.palette.text.secondary,
//color: theme.taskPromptTextColor,
overflow: "auto",
display: "block",
textOverflow: "ellipsis",
@@ -77,7 +90,7 @@ export const StyledPaper = styled(Paper)((
},
[`& .${classes.taskAndTimeDisplay}`]: {
fontSize: theme.typography.pxToRem(12),
color: theme.palette.text.secondary,
color: theme.taskPromptTextColor,
overflow: "hidden",
textOverflow: "ellipsis",
maxWidth: "100%",
@@ -88,7 +101,7 @@ export const StyledPaper = styled(Paper)((
},
[`& .${classes.secondaryHeadingExpanded}`]: {
fontSize: theme.typography.pxToRem(15),
//color: theme.palette.text.secondary,
//color: theme.taskPromptTextColor,
display: "block",
overflow: "auto",
maxWidth: "100%",
@@ -127,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,
@@ -150,10 +165,10 @@ export const StyledAccordionSummary = styled(AccordionSummary)((
}));
function TaskDisplayPreMemo({task, me, filterOptions, newlyIssuedTasks}){
function TaskDisplayPreMemo({task, me, filterOptions, newlyIssuedTasks, collapseAllRequest}){
return (
<TaskRow me={me} task={task} newlyIssuedTasks={newlyIssuedTasks} filterOptions={filterOptions}
indentLevel={0} />
indentLevel={0} collapseAllRequest={collapseAllRequest} />
);
}
export const TaskDisplay = React.memo(TaskDisplayPreMemo);
@@ -212,14 +227,16 @@ const ColoredTaskDisplay = ({task, theme, children, expanded}) => {
setThemeColor(theme.palette.warning.main);
}else if(task.status.toLowerCase().includes("processing")){
setThemeColor(theme.palette.warning.main);
}else if(task.status === "completed" || (task.status === "success" && task.completed)){
}else if(task.status === "completed" || task.completed){
setThemeColor(theme.palette.success.main);
}else{
setThemeColor(theme.palette.info.main);
}
}, [task.status, task.completed])
return(
<span style={{display: "flex", margin: 0, borderWidth: 0, padding: 0, minHeight: "48px", alignItems: "center", height: "100%", borderLeft: "6px solid " + themeColor, paddingLeft: "5px", width: "100%", borderTopLeftRadius: "4px", borderBottomLeftRadius: expanded ? 0 : "4px"}}>
<span style={{display: "flex", margin: 0, borderWidth: 0, padding: 0, minHeight: "48px", alignItems: "center",
height: "100%", borderLeft: "6px solid " + themeColor, paddingLeft: "5px", width: "100%", maxWidth: "100%",
borderTopLeftRadius: "4px", borderBottomLeftRadius: expanded ? 0 : "4px"}}>
{children}
</span>
)
@@ -228,16 +245,20 @@ 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);
const [alertBadges, setAlertBadges] = React.useState(0);
const initialHideUsernameValue = useMythicSetting({setting_name: "hideUsernames", default_value: "false"});
const initialShowIPValue = useMythicSetting({setting_name: "showIP", default_value: "false"});
const initialHideUsernameValue = GetMythicSetting({setting_name: "hideUsernames", default_value: operatorSettingDefaults.hideUsernames});
const initialShowIPValue = GetMythicSetting({setting_name: "showIP", default_value: operatorSettingDefaults.showIP});
const ipValue = JSON.parse(task.callback.ip)[0];
const initialShowHostnameValue = useMythicSetting({setting_name: "showHostname", default_value: "false"});
const initialShowCallbackGroupsValue = useMythicSetting({setting_name: "showCallbackGroups", default_value: "false"});
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) => {
evt.stopPropagation();
setDisplayComment(!displayComment);
@@ -250,85 +271,108 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh
if(onClick){
onClick(e);
}
//preventPropagation(e);
}
const onClickKillIcon = (e, open) => {
if(e){
e.stopPropagation();
}
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 ? (
{displayComment && (
<React.Fragment>
<Typography className={classes.taskAndTimeDisplay} onClick={preventPropagation}>{task.commentOperator.username}</Typography><br/>
<Typography className={classes.heading} onClick={preventPropagation}>{task.comment}</Typography>
</React.Fragment>
) : null}
<div style={{lineHeight: 0}}>
)}
<div style={{lineHeight: 0}} onClick={onLocalClick}>
<Typography className={classes.taskAndTimeDisplay} onClick={preventPropagation}>
[{toLocalTime(task.timestamp, me?.user?.view_utc_time || false)}]
[{toLocalTime(displayTimestamp, me?.user?.view_utc_time || false)}]
{" / "}
<span style={{}}>
{task.has_intercepted_response &&
<>
<MythicStyledTooltip
title={"This task has responses that have been intercepted and changed due to a workflow container"}>
<IconButton style={{padding: 0}} color={"secondary"}>
<CropRotateTwoToneIcon fontSize={"small"}/>
</IconButton>
</MythicStyledTooltip>
{"/ "}
</>
}
{task?.eventstepinstance !== null &&
<>
<MythicStyledTooltip title={"Task created via Eventing, click to view entire event flow in separate page"} >
<IconButton component={Link} href={'/new/eventing?eventgroup=' +
task?.eventstepinstance?.eventgroupinstance?.eventgroup?.id +
"&eventgroupinstance=" + task?.eventstepinstance?.eventgroupinstance?.id
} target={"_blank"} style={{padding: 0}}
color="inherit" disableFocusRipple={true}
{task.has_intercepted_response &&
<span style={{display:"inline-flex", whiteSpace: "pre-wrap"}}>
<MythicStyledTooltip
title={"This task has responses that have been intercepted and changed due to a workflow container"}>
<IconButton color={"inherit"} style={{padding: 0}} disableFocusRipple={true}
disableRipple={true}>
<PlayCircleFilledTwoToneIcon />
<CropRotateTwoToneIcon style={{height: "15px", cursor: "default"}}/>
</IconButton>
</MythicStyledTooltip>
{"/ "}
</>
}
<MythicStyledTooltip title={"View Task in separate page"} >
<Link style={{wordBreak: "break-all"}} color={"textPrimary"} underline={"always"} target={"_blank"}
href={"/new/task/" + task.display_id}>{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: "12px", marginLeft: "5px"}} />
</MythicStyledTooltip>
}
</span>
{" "}
<GetOperatorDisplay initialHideUsernameValue={initialHideUsernameValue} task={task}/>
{" / "}
<MythicStyledTooltip title={"View Callback in separate page"}>
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
href={"/new/callbacks/" + task.callback.display_id}>{task.callback.display_id}</Link>
<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} />
@@ -340,7 +384,7 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh
enterDelay={2000}
placement={"top"}
title={(task?.command?.cmd || task.command_name) + " " + task.display_params} >
<Typography className={classes.heading} onClick={onLocalClick} >
<Typography className={classes.heading} style={{color: theme.taskPromptCommandTextColor}} onClick={onLocalClick} >
{(task?.command?.cmd || task.command_name) + " " + task.display_params}
</Typography>
</MythicStyledTooltip>
@@ -348,15 +392,28 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh
</div>
</div>
</div>
{openKillTaskButton.open &&
<TaskFromUIButton ui_feature={"task:job_kill"}
callback_id={task.callback?.id}
display_id={task.callback?.display_id}
parameters={task.agent_task_id}
openDialog={false}
getConfirmation={true}
acceptText={"KILL JOB"}
selectCallback={false}
onTasked={({tasked}) => onClickKillIcon(null, false)}/>
}
</ColoredTaskDisplay>
)
}
const TaskRow = ({task, filterOptions, me, newlyIssuedTasks, indentLevel}) => {
const TaskRow = ({task, filterOptions, me, newlyIssuedTasks, indentLevel, collapseAllRequest}) => {
const [dropdownOpen, setDropdownOpen] = React.useState(false);
const [taskingData, setTaskingData] = React.useState([]);
const [shouldDisplay, setShouldDisplay] = React.useState(true);
const [displayChildren, setDisplayChildren] = React.useState(false);
useSubscription(getSubTaskingQuery, {
const hideBrowserTasking = GetMythicSetting({setting_name: "hideBrowserTasking", default_value: operatorSettingDefaults.hideBrowserTasking});
useSubscription(getSubTaskingQuery, {
variables: {task_id: task.id},
onData: ({data}) => {
//console.log(subscriptionData);
@@ -383,6 +440,10 @@ const TaskRow = ({task, filterOptions, me, newlyIssuedTasks, indentLevel}) => {
"parameterString": onlyParameters,
"hideErrors": hideErrors
}); */
if(hideBrowserTasking && task.tasking_location.includes("_browser")){
setShouldDisplay(false);
return;
}
if(task.display_params.includes("help") && task.operator.username !== me.user.username){
setShouldDisplay(false);
return;
@@ -451,6 +512,10 @@ const TaskRow = ({task, filterOptions, me, newlyIssuedTasks, indentLevel}) => {
if(window.getSelection().toString() !== ""){
return;
}
if(event){
event.stopPropagation();
event.preventDefault();
}
setDropdownOpen(!dropdownOpen);
}, [dropdownOpen]);
const toggleDisplayChildren = React.useCallback( (event, expanded) => {
@@ -458,7 +523,16 @@ const TaskRow = ({task, filterOptions, me, newlyIssuedTasks, indentLevel}) => {
return;
}
setDisplayChildren(!displayChildren);
if(event){
event.stopPropagation();
event.preventDefault();
}
}, [displayChildren]);
useEffect( () => {
if(collapseAllRequest > 0){
setDropdownOpen(false);
}
}, [collapseAllRequest]);
/*
useEffect( () => {
if(!isFetchingSubtasks && task.tasks.length > 0){
@@ -487,6 +561,8 @@ const TaskRowFlat = ({task, filterOptions, me, onSelectTask, showOnSelectTask, s
const [taskingData, setTaskingData] = React.useState([]);
const [shouldDisplay, setShouldDisplay] = React.useState(true);
const [displayChildren, setDisplayChildren] = React.useState(false);
const hideBrowserTasking = GetMythicSetting({setting_name: "hideBrowserTasking", default_value: operatorSettingDefaults.hideBrowserTasking});
useSubscription(getSubTaskingQuery, {
variables: {task_id: task.id},
onData: ({data}) => {
@@ -512,6 +588,10 @@ const TaskRowFlat = ({task, filterOptions, me, onSelectTask, showOnSelectTask, s
"everythingButList": everythingBut,
"parameterString": onlyParameters
}); */
if(hideBrowserTasking && task.tasking_location.includes("browser")){
setShouldDisplay(false);
return;
}
if(task.display_params.includes("help") && task.operator.username !== me.user.username){
setShouldDisplay(false);
return;
@@ -602,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}
/>
@@ -623,10 +706,133 @@ const TaskRowFlat = ({task, filterOptions, me, onSelectTask, showOnSelectTask, s
)
)
}
const TaskLabel = ({task, dropdownOpen, toggleTaskDropdown, me, newlyIssuedTasks, displayChildren, toggleDisplayChildren}) => {
const [fromNow, setFromNow] = React.useState(getSkewedNow());
const theme = useTheme();
const prevResponseMaxId = useRef(0);
useEffect( () => {
//console.log("in use effect", prevResponseCount.current, props.task.responses);
let currentData = task.response_count;
if(!dropdownOpen){
// only automatically open the dropdown if a new response comes in while we're looking
if((new Date(task.timestamp + "Z")) >= fromNow){
if(prevResponseMaxId.current === 0 && currentData > 0){
toggleTaskDropdown();
prevResponseMaxId.current = currentData;
}
} else if(newlyIssuedTasks !== undefined) {
let newIndex = newlyIssuedTasks.findIndex( (e) => e === task.id);
if (newIndex > -1) {
toggleTaskDropdown();
prevResponseMaxId.current = currentData;
newlyIssuedTasks.splice(newIndex, 1);
}
}
}else{
prevResponseMaxId.current = currentData;
}
}, [task.response_count, dropdownOpen]);
const scrollContent = (node, isAppearing) => {
// only auto-scroll if you issued the task
if(task.operator.username === (me?.user?.username || "")){
let el = document.getElementById(`taskingPanel${task.callback_id}`);
if(el && el.scrollHeight - el.scrollTop - el.clientHeight < 100){
document.getElementById(`scrolltotask${task.id}`)?.scrollIntoView({
//behavior: "smooth",
block: "start",
inline: "start"
});
}
}
}
return (
<StyledPaper className={classes.root + " no-box-shadow"} elevation={5} id={`taskHeader-${task.id}`}>
<Accordion TransitionProps={{ unmountOnExit: true, onEnter: scrollContent }} defaultExpanded={false}
onChange={toggleTaskDropdown} expanded={dropdownOpen}
style={{backgroundColor: "unset", backgroundImage: "unset"}}
>
<StyledAccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={`panel1c-content-task-${task.id}`}
id={`panel1c-header-${task.id}`}
classes={accordionClasses}
>
<ColoredTaskLabel theme={theme} task={task} me={me} taskDivID={'scrolltotask' + task.id}
displayChildren={displayChildren} toggleDisplayChildren={toggleDisplayChildren}
expanded={dropdownOpen}
/>
</StyledAccordionSummary>
<TaskDisplayContainer key={task.id} me={me} task={task} />
</Accordion>
</StyledPaper>
);
}
export const getLabelText = (task, graphView) => {
if(graphView){
if(task.display_params.length > 50){
return (task?.command?.cmd || task.command_name) + " " + task.display_params.substring(0, 50) + "...";
}
return (task?.command?.cmd || task.command_name) + " " + task.display_params.substring(0, 50);
}
return (task?.command?.cmd || task.command_name) + " " + task.display_params;
}
export const TaskLabelFlat = ({task, me, showOnSelectTask, onSelectTask, graphView, displayChildren, toggleDisplayChildren}) => {
const theme = useTheme();
useLayoutEffect( () => {
if(task.operator.username === (me?.user?.username || "")){
scrollContent();
}
}, [])
const scrollContent = (node, isAppearing) => {
// only auto-scroll if you issued the task
if(task.operator.username === (me?.user?.username || "")){
let el = document.getElementById(`taskingPanelSplit${task.callback_id}`);
if(el && el.scrollHeight - el.scrollTop - el.clientHeight < 100){
document.getElementById(`scrolltotasksplit${task.id}`)?.scrollIntoView({
//behavior: "smooth",
block: "start",
inline: "start"
});
}
}
}
const onClickEntry = (e) => {
if(showOnSelectTask){
onSelectTask(e);
}
}
return(
<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}
>
<ColoredTaskLabel theme={theme} task={task} me={me} taskDivID={`scrolltotasksplit${task.id}`} onClick={onClickEntry}
displayChildren={displayChildren} toggleDisplayChildren={toggleDisplayChildren} expanded={false}
/>
</StyledPaper>
)
}
const ColoredTaskDisplayConsole = ({task, theme, children, expanded}) => {
return(
<span style={{display: "flex", margin: 0, borderWidth: 0, padding: 0, minHeight: "30px", alignItems: "center",
height: "100%", paddingLeft: "5px", width: "100%",
borderTopLeftRadius: "4px", borderBottomLeftRadius: expanded ? 0 : "4px"}}>
{children}
</span>
)
}
const TaskRowConsole = ({task, filterOptions, me, newlyIssuedTasks, indentLevel}) => {
const [dropdownOpen, setDropdownOpen] = React.useState(false);
const [taskingData, setTaskingData] = React.useState([]);
const [shouldDisplay, setShouldDisplay] = React.useState(true);
const hideBrowserTasking = GetMythicSetting({setting_name: "hideBrowserTasking", default_value: operatorSettingDefaults.hideBrowserTasking});
useSubscription(getSubTaskingQuery, {
variables: {task_id: task.id},
onData: ({data}) => {
@@ -654,6 +860,10 @@ const TaskRowConsole = ({task, filterOptions, me, newlyIssuedTasks, indentLevel}
"parameterString": onlyParameters,
"hideErrors": hideErrors
}); */
if(hideBrowserTasking && task.tasking_location.includes("browser")){
setShouldDisplay(false);
return;
}
if(task.display_params.includes("help") && task.operator.username !== me.user.username){
setShouldDisplay(false);
return;
@@ -718,21 +928,15 @@ const TaskRowConsole = ({task, filterOptions, me, newlyIssuedTasks, indentLevel}
setShouldDisplay(true);
}
}, [filterOptions, task.comment, task.command, task.display_params, task.operator.username]);
const toggleTaskDropdown = React.useCallback( (event, expanded) => {
if(window.getSelection().toString() !== ""){
return;
}
setDropdownOpen(!dropdownOpen);
}, [dropdownOpen]);
// marginLeft: (indentLevel * 10) + "px"
return (
shouldDisplay ? (
<div style={{marginLeft: (indentLevel * 10) + "px"}}>
<div style={{}}>
<TaskLabelConsole me={me} task={task} newlyIssuedTasks={newlyIssuedTasks} />
{
taskingData.map( (tsk) => (
<TaskRowConsole key={"taskrow: " + tsk.id} me={me} task={tsk}
filterOptions={filterOptions} indentLevel={indentLevel+1}/>
filterOptions={filterOptions} indentLevel={indentLevel+1}/>
))
}
</div>
@@ -740,118 +944,133 @@ const TaskRowConsole = ({task, filterOptions, me, newlyIssuedTasks, indentLevel}
) : null
)
}
const TaskLabel = ({task, dropdownOpen, toggleTaskDropdown, me, newlyIssuedTasks, displayChildren, toggleDisplayChildren}) => {
const [fromNow, setFromNow] = React.useState(new Date());
const theme = useTheme();
const prevResponseMaxId = useRef(0);
export const ColoredTaskLabelConsole = ({task, theme, me, taskDivID, onClick, displayChildren, toggleDisplayChildren, expanded }) => {
const initialHideUsernameValue = GetMythicSetting({setting_name: "hideUsernames", default_value: operatorSettingDefaults.hideUsernames});
const initialTaskTimestampDisplayField = GetMythicSetting({setting_name: "taskTimestampDisplayField", default_value: operatorSettingDefaults.taskTimestampDisplayField});
const displayTimestamp = task[initialTaskTimestampDisplayField] ? task[initialTaskTimestampDisplayField] : task.timestamp;
const initialShowHostnameValue = GetMythicSetting({setting_name: "showHostname", default_value: operatorSettingDefaults.showHostname});
const initialShowIPValue = GetMythicSetting({setting_name: "showIP", default_value: operatorSettingDefaults.showIP});
const [openKillTaskButton, setOpenKillTaskButton] = React.useState({open: false});
const preventPropagation = (e) => {
e.stopPropagation();
//e.preventDefault();
}
const onClickKillIcon = (e, open) => {
if(e){
e.stopPropagation();
}
setOpenKillTaskButton({open: open});
}
const [themeColor, setThemeColor] = React.useState(theme.palette.info.main);
useEffect( () => {
//console.log("in use effect", prevResponseCount.current, props.task.responses);
let currentData = task.response_count;
if(!dropdownOpen){
// only automatically open the dropdown if a new response comes in while we're looking
if((new Date(task.timestamp + "Z")) >= fromNow){
if(prevResponseMaxId.current === 0 && currentData > 0){
toggleTaskDropdown();
prevResponseMaxId.current = currentData;
}
} else if(newlyIssuedTasks !== undefined) {
let newIndex = newlyIssuedTasks.findIndex( (e) => e === task.id);
if (newIndex > -1) {
toggleTaskDropdown();
prevResponseMaxId.current = currentData;
newlyIssuedTasks.splice(newIndex, 1);
}
}
if(task.status.toLowerCase().includes("error")){
setThemeColor(theme.palette.error.main);
}else if(task.status.toLowerCase() === "cleared"){
setThemeColor(theme.palette.warning.main);
}else if(task.status === "submitted"){
setThemeColor(theme.palette.info.main);
}else if(task.opsec_pre_blocked && !task.opsec_pre_bypassed){
setThemeColor(theme.palette.warning.main);
}else if(task.opsec_post_blocked && !task.opsec_post_bypassed){
setThemeColor(theme.palette.warning.main);
}else if(task.status.toLowerCase().includes("processing")){
setThemeColor(theme.palette.warning.main);
}else if(task.status === "completed" || (task.status === "success" && task.completed)){
setThemeColor(theme.palette.success.main);
}else{
prevResponseMaxId.current = currentData;
setThemeColor(theme.palette.info.main);
}
}, [task.response_count, dropdownOpen]);
const scrollContent = (node, isAppearing) => {
// only auto-scroll if you issued the task
if(task.operator.username === (me?.user?.username || "")){
let el = document.getElementById(`taskingPanel${task.callback_id}`);
if(el && el.scrollHeight - el.scrollTop - el.clientHeight < 100){
document.getElementById(`scrolltotask${task.id}`)?.scrollIntoView({
//behavior: "smooth",
block: "start",
inline: "start"
});
}
}
}
}, [task.status, task.completed])
return (
<StyledPaper className={classes.root + " no-box-shadow"} elevation={5} id={`taskHeader-${task.id}`}>
<Accordion TransitionProps={{ unmountOnExit: true, onEnter: scrollContent }} defaultExpanded={false}
onChange={toggleTaskDropdown} expanded={dropdownOpen}
style={{backgroundColor: "unset", backgroundImage: "unset"}}
>
<StyledAccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={`panel1c-content-task-${task.id}`}
id={`panel1c-header-${task.id}`}
classes={accordionClasses}
>
<ColoredTaskLabel theme={theme} task={task} me={me} taskDivID={'scrolltotask' + task.id}
displayChildren={displayChildren} toggleDisplayChildren={toggleDisplayChildren}
expanded={dropdownOpen}
/>
</StyledAccordionSummary>
<TaskDisplayContainer me={me} task={task} />
</Accordion>
</StyledPaper>
);
}
export const getLabelText = (task, graphView) => {
if(graphView){
if(task.display_params.length > 50){
return (task?.command?.cmd || task.command_name) + " " + task.display_params.substring(0, 50) + "...";
}
return (task?.command?.cmd || task.command_name) + " " + task.display_params.substring(0, 50);
}
return (task?.command?.cmd || task.command_name) + " " + task.display_params;
}
export const TaskLabelFlat = ({task, me, showOnSelectTask, onSelectTask, graphView, displayChildren, toggleDisplayChildren}) => {
const theme = useTheme();
useLayoutEffect( () => {
if(task.operator.username === (me?.user?.username || "")){
scrollContent();
}
}, [])
const scrollContent = (node, isAppearing) => {
// only auto-scroll if you issued the task
if(task.operator.username === (me?.user?.username || "")){
let el = document.getElementById(`taskingPanelSplit${task.callback_id}`);
if(el && el.scrollHeight - el.scrollTop - el.clientHeight < 100){
document.getElementById(`scrolltotasksplit${task.id}`)?.scrollIntoView({
//behavior: "smooth",
block: "start",
inline: "start"
});
}
}
}
const onClickEntry = (e) => {
if(showOnSelectTask){
onSelectTask(e);
}
}
return(
<StyledPaper className={task.selected ? classes.root + " selectedTask no-box-shadow" : classes.root}
elevation={5} style={{marginRight: 0, cursor: "pointer"}} id={`taskHeader-${task.id}`}
onClick={onClickEntry}
>
<ColoredTaskLabel theme={theme} task={task} me={me} taskDivID={`scrolltotasksplit${task.id}`} onClick={onClickEntry}
displayChildren={displayChildren} toggleDisplayChildren={toggleDisplayChildren} expanded={false}
/>
</StyledPaper>
)
<ColoredTaskDisplayConsole task={task} theme={theme} expanded={expanded}>
<div id={taskDivID} style={{ width: "100%" }}>
<Typography sx={{ color: theme.taskPromptTextColor, display: "flex", alignItems: "center" }} style={{ fontFamily: "monospace" }}>
<span style={{ fontFamily: "monospace" }}>┌──[</span>
<AccessTimeIcon fontSize="small" style={{ margin: "0 3px 0 0px", verticalAlign: "middle" }} />
{toLocalTimeShort(displayTimestamp, me?.user?.view_utc_time || false)}
{"]"}
{!initialHideUsernameValue &&
<>
{"-["}
<PersonOutlineIcon fontSize="small" style={{ margin: "0 3px 0 0px", verticalAlign: "middle" }} />
{task.operator.username}
{"]"}
</>
}
{"-["}
<NumbersIcon fontSize="small" style={{ margin: "0 3px 0 0px", verticalAlign: "middle" }} />
<MythicStyledTooltip title={"View Task in separate page"} >
<Link style={{wordBreak: "break-all", color: theme.taskPromptTextColor,}} underline={"always"} target={"_blank"}
href={"/new/task/" + task.display_id}>T-{task.display_id}</Link>
</MythicStyledTooltip>
{!task.completed && task.status_timestamp_processing &&
<>
<MythicStyledTooltip title={"Task the agent to kill this task"} >
<FontAwesomeIcon size={"sm"} icon={faSkullCrossbones} onClick={(e) => onClickKillIcon(e, true)}
style={{cursor: "pointer", height: "15px", marginLeft: "5px"}} />
</MythicStyledTooltip>
</>
}
{"]"}
{initialShowHostnameValue ? (
<>
{"-["}
<ComputerIcon fontSize="small" style={{ margin: "0 3px 0 0px", verticalAlign: "middle" }} />
{task.callback.host}
{"]"}
</>) : null}
{initialShowIPValue ? (
<>
{"-["}
<PublicIcon fontSize="small" style={{ margin: "0 3px 0 0px", verticalAlign: "middle" }} />
{JSON.parse(task.callback.ip)[0]}
{"]"}
</> ) : null}
{(task.opsec_pre_blocked && !task.opsec_pre_bypassed) ? (
<>
{"-["}
<WarningAmberIcon fontSize="small" style={{ margin: "0 3px 0 0px", verticalAlign: "middle", color: theme.palette.warning.main }} />
{"OPSEC BLOCKED (PRE)]"}
</>) : null}
{(task.opsec_post_blocked && !task.opsec_post_bypassed) ? (
<>
{"-["}
<WarningAmberIcon fontSize="small" style={{ margin: "0 3px 0 0px", verticalAlign: "middle", color: theme.palette.warning.main }} />
{"OPSEC BLOCKED (POST)]"}
</>) : null}
{")"}
</Typography>
<MythicStyledTooltip title={task.status}>
<span style={{ fontFamily: "monospace", marginRight: 2, color: theme.taskPromptTextColor }}>└─</span>
<b style={{ fontFamily: "monospace", color: themeColor, fontWeight: 600 }}>{">_"}</b>{" "}
</MythicStyledTooltip>
<MythicStyledTooltip
title={(task?.command?.cmd || task.command_name) + " " + task.display_params}
maxWidth="calc(80vw)"
enterDelay={2000}
placement="top">
<Typography
sx={{ fontSize: 15, display: "flex", alignItems: "center", marginLeft: 0}}
style={{ fontFamily: "monospace" }}>
<span style={{ marginLeft: 4, color: theme.taskPromptCommandTextColor }}>
<b>{(task?.command?.cmd || task.command_name)}</b> {task.display_params}
</span>
</Typography>
</MythicStyledTooltip>
</div>
{openKillTaskButton.open &&
<TaskFromUIButton ui_feature={"task:job_kill"}
callback_id={task.callback?.id}
display_id={task.callback?.display_id}
parameters={task.agent_task_id}
openDialog={false}
getConfirmation={true}
acceptText={"KILL JOB"}
selectCallback={false}
onTasked={({tasked}) => onClickKillIcon(null, false)}/>
}
</ColoredTaskDisplayConsole>
);
}
const TaskLabelConsole = ({task, me}) => {
const theme = useTheme();
@@ -876,9 +1095,10 @@ const TaskLabelConsole = ({task, me}) => {
}
return (
<StyledPaper className={classes.root + " no-box-shadow"} elevation={5} style={{marginRight: 0}} id={`taskHeader-${task.id}`}>
<ColoredTaskLabel theme={theme} task={task} me={me} taskDivID={`scrolltotaskconsole${task.id}`} expanded={true}/>
<StyledPaper className={classes.root + " no-box-shadow no-border"} elevation={5} style={{marginRight: 0, marginBottom: "5px"}} id={`taskHeader-${task.id}`}>
<ColoredTaskLabelConsole theme={theme} task={task} me={me} taskDivID={`scrolltotaskconsole${task.id}`} expanded={true}/>
<TaskDisplayContainerConsole me={me} task={task} />
<div style={{borderBottom: "0px dashed grey", width: "100%", height: "5px", marginTop: "5px"}}/>
</StyledPaper>
);
}
}
@@ -35,6 +35,12 @@ import html2canvas from 'html2canvas';
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!){
@@ -68,7 +74,7 @@ export const TaskDisplayContainer = ({task, me}) => {
const responseRef = React.useRef(null);
useEffect( () => {
setCommandID(task.command === null ? 0 : task.command.id);
}, [task.command]);
}, [task.command?.id]);
const toggleViewBrowserScript = React.useCallback( () => {
setViewBrowserScript(!viewBrowserScript);
}, [viewBrowserScript]);
@@ -93,7 +99,7 @@ export const TaskDisplayContainer = ({task, me}) => {
bottom: "25px", right: "4px", height: 0}}
fabStyle={{ }}
viewAllOutput={selectAllOutput}/>
<Grid item xs={12}>
<Grid size={12}>
<ResponseDisplay
task={task}
me={me}
@@ -118,7 +124,7 @@ export const TaskDisplayContainerFlat = ({task, me}) => {
setSearchOutput(false);
setSelectAllOutput(false);
setViewBrowserScript(true);
}, [task.command, task.id]);
}, [task.command?.id, task.id]);
const toggleViewBrowserScript = React.useCallback( () => {
setViewBrowserScript(!viewBrowserScript);
}, [viewBrowserScript]);
@@ -130,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}
@@ -142,7 +148,7 @@ export const TaskDisplayContainerFlat = ({task, me}) => {
expand={true}
responseRef={responseRef}
/>
<SpeedDialDisplayGeneric toggleViewBrowserScript={toggleViewBrowserScript}
<SideDisplayGeneric toggleViewBrowserScript={toggleViewBrowserScript}
toggleSelectAllOutput={toggleSelectAllOutput}
toggleOpenSearch={toggleOpenSearch}
taskData={task}
@@ -159,33 +165,13 @@ export const TaskDisplayContainerConsole = ({task, me}) => {
const [viewBrowserScript, setViewBrowserScript] = React.useState(true);
const [commandID, setCommandID] = React.useState(task?.command?.id || 0);
const [searchOutput, setSearchOutput] = React.useState(false);
const [selectAllOutput, setSelectAllOutput] = React.useState(false);
const responseRef = React.useRef(null);
useEffect( () => {
setCommandID(task.command === null ? 0 : task.command.id);
}, [task.command]);
const toggleViewBrowserScript = React.useCallback( () => {
setViewBrowserScript(!viewBrowserScript);
}, [viewBrowserScript]);
const toggleSelectAllOutput = React.useCallback( () => {
setSelectAllOutput(!selectAllOutput);
}, [selectAllOutput]);
const toggleOpenSearch = React.useCallback( () => {
setSearchOutput(!searchOutput);
}, [searchOutput]);
}, [task.command?.id]);
return (
<>
<SpeedDialDisplayGeneric toggleViewBrowserScript={toggleViewBrowserScript}
toggleSelectAllOutput={toggleSelectAllOutput}
toggleOpenSearch={toggleOpenSearch}
taskData={task}
me={me}
viewBrowserScript={viewBrowserScript}
responseRef={responseRef}
style={{ bottom: "25px", right: "4px", zIndex: 2, height: "0px", position: "relative", display: "flex", flexDirection: "row-reverse"}}
fabStyle={{ }}
viewAllOutput={true}/>
<ResponseDisplayConsole
task={task}
me={me}
@@ -198,23 +184,278 @@ export const TaskDisplayContainerConsole = ({task, me}) => {
);
}
// the base64 decode function to handle unicode was pulled from the following stack overflow post
// https://stackoverflow.com/a/30106551
function b64DecodeUnicode(str) {
// Going backwards: from bytestream, to percent-encoding, to original string.
//console.log("decoding", str);
try{
return decodeURIComponent(window.atob(str));
}catch(error){
//console.log("Failed to base64 decode response", error)
return atob(str);
}
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);
@@ -306,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"} />}
@@ -421,7 +677,7 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput
icon={<KeyboardIcon/>}
arrow
tooltipPlacement={"top"}
tooltipTitle={"View All Parameters"}
tooltipTitle={"View All Parameters And Timestamps"}
onClick={()=>{setOpenParametersDialog(true);setOpenSpeedDial(false);}}
/>
<SpeedDialAction
@@ -431,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}}/>}
@@ -6,6 +6,7 @@ import DialogTitle from '@mui/material/DialogTitle';
import MythicTextField from '../../MythicComponents/MythicTextField';
import {useQuery, gql, useMutation} from '@apollo/client';
import { snackActions } from '../../utilities/Snackbar';
import {MythicModifyStringDialog} from "../../MythicComponents/MythicDialog";
const updateOpsecRequestMutation = gql`
mutation requestOpsecBypass ($task_id: Int!) {
@@ -83,6 +84,7 @@ export function TaskOpsecDialog(props) {
console.error(error);
return <div>Error! {error.message}</div>;
}
const requestAvailable = (opsecData.opsec_pre_blocked === true && !opsecData.opsec_pre_bypassed) || (opsecData.opsec_post_blocked === true && !opsecData.opsec_post_bypassed);
const onRequestSubmit = () => {
//console.log(props.task_id);
updateOpsecRequest({variables: {task_id: props.task_id}});
@@ -91,20 +93,15 @@ export function TaskOpsecDialog(props) {
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Request OPSEC Bypass</DialogTitle>
<DialogContent dividers={true}>
<MythicTextField multiline={true} onChange={()=>{}} value={opsecMessage} />
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
Close
</Button>
{(opsecData.opsec_pre_blocked === true && !opsecData.opsec_pre_bypassed) || (opsecData.opsec_post_blocked === true && !opsecData.opsec_post_bypassed) ?
(
<Button onClick={onRequestSubmit} variant="contained" color="warning">Submit Bypass Request</Button>
) : null }
</DialogActions>
<MythicModifyStringDialog title={`Request OPSEC Bypass`}
onClose={props.onClose}
maxRows={40}
wrap={true}
value={opsecMessage}
onSubmit={requestAvailable ? onRequestSubmit : undefined}
onSubmitText={"Submit Bypass Request"}
dontCloseOnSubmit={true}
/>
</React.Fragment>
);
}
@@ -21,6 +21,7 @@ import {Backdrop, CircularProgress} from '@mui/material';
import Divider from '@mui/material/Divider';
import {b64DecodeUnicode} from './ResponseDisplay';
import {snackActions} from "../../utilities/Snackbar";
import {MythicDraggableDialogTitle} from "../../MythicComponents/MythicDraggableDialogTitle";
//if we need to get all the loaded commands for the callback and filter, use this
const GetLoadedCommandsQuery = gql`
@@ -105,7 +106,7 @@ query getAllEdgesQuery($callback_id: Int!){
// get all payloads query
const getAllPayloadsQuery = gql`
query getAllPayloadsQuery($operation_id: Int!){
payload(where: {deleted: {_eq: false}, build_phase: {_eq: "success"}, operation_id: {_eq: $operation_id}}) {
payload(where: {deleted: {_eq: false}, build_phase: {_eq: "success"}, operation_id: {_eq: $operation_id}}, order_by: {id: desc}) {
id
description
uuid
@@ -124,6 +125,7 @@ query getAllPayloadsQuery($operation_id: Int!){
filemetum {
id
filename_text
timestamp
}
buildparameterinstances {
value
@@ -140,7 +142,7 @@ query getAllPayloadsQuery($operation_id: Int!){
// get all payloads on hosts
const getAllPayloadsOnHostsQuery = gql`
query getAllPayloadsOnHostsQuery($operation_id: Int!){
payloadonhost(where: {deleted: {_eq: false}, operation_id: {_eq: $operation_id}, payload: {c2profileparametersinstances: {c2profile: {is_p2p: {_eq: true}}}}}) {
payloadonhost(where: {deleted: {_eq: false}, operation_id: {_eq: $operation_id}, payload: {c2profileparametersinstances: {c2profile: {is_p2p: {_eq: true}}}}}, order_by: {id: desc}) {
host
id
payload {
@@ -255,6 +257,7 @@ query getCommandQuery($id: Int!){
parameter_group_name
display_name
cli_name
verifier_regex
}
}
}
@@ -262,7 +265,7 @@ query getCommandQuery($id: Int!){
// use this to get all the credentials for the command we're trying to execute
const getCredentialsQuery = gql`
query getCredentialsQuery($operation_id: Int!){
credential(where: {deleted: {_eq: false}, operation_id: {_eq: $operation_id}}){
credential(where: {deleted: {_eq: false}, operation_id: {_eq: $operation_id}}, order_by: {id: desc}){
account
comment
credential_text
@@ -382,9 +385,8 @@ export function TaskParametersDialog(props) {
const addedCredential = (credential) => {
getAllCredentials({variables: {operation_id: props.operation_id}});
}
const intersect = (a, b) => {
let setB = new Set(b);
return [...new Set(a)].filter(x => setB.has(x));
const removedCredential = (credential) => {
getAllCredentials({variables: {operation_id: props.operation_id}});
}
const setSubmenuOpenPreventTasking = (open) => {
setSubmenuOpenPreventTask(open);
@@ -515,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;
}
}
@@ -539,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;
}
}
@@ -557,7 +575,7 @@ export function TaskParametersDialog(props) {
}
}
if(parsedParameterName){
return [...prev, {...cmd, choices: choices, value: props.command.parsedParameters[parsedParameterName]}];
return [...prev, {...cmd, choices: choices, value: props.command.parsedParameters[parsedParameterName], default_value: defaultV}];
}else{
return [...prev, {...cmd, choices: choices, default_value: defaultV, value: defaultV}];
}
@@ -604,11 +622,7 @@ export function TaskParametersDialog(props) {
}
}, []).sort((a,b) => {
if(a.filemetum.filename_text === b.filemetum.filename_text){
return a.id < b.id ? 1 : -1
}else{
return a.filemetum.filename_text < b.filemetum.filename_text ? 1 : -1
}
return a.id < b.id ? 1 : -1;
});
const callbacksOrganized = loadedAllPayloadsOnHostsLoading.callback.reduce( (prevn, entry) => {
@@ -772,11 +786,9 @@ export function TaskParametersDialog(props) {
}, []);
payloads.sort((a,b) => {
if(a.filemetum.filename_text === b.filemetum.filename_text){
return a.id < b.id ? 1 : -1
}else{
return a.filemetum.filename_text < b.filemetum.filename_text ? 1 : -1
}
let aTimestamp = new Date(a.filemetum.timestamp);
let bTimestamp = new Date(b.filemetum.timestamp);
return aTimestamp < bTimestamp ? 1 : -1;
});
//now filter the payloads based on supported_agents and supported_agent_build_parameters
if(payloads.length > 0){
@@ -896,7 +908,7 @@ export function TaskParametersDialog(props) {
}
}
setBackdropOpen(false);
props.onSubmit(props.command.cmd, JSON.stringify(collapsedParameters), newFileUUIDs, selectedParameterGroup, props.command?.payload_type?.name);
props.onSubmit(commandInfo.cmd, JSON.stringify(collapsedParameters), newFileUUIDs, selectedParameterGroup, commandInfo?.payloadtype?.name);
}
const onAgentConnectAddNewPayloadOnHost = (host, payload) => {
@@ -921,10 +933,52 @@ export function TaskParametersDialog(props) {
const onChangeParameterGroup = (event) => {
setSelectedParameterGroup(event.target.value);
}
const getOtherParameters = () => {
let collapsedParameters = {};
for(const param of parameters){
switch(param.type){
case "String":
case "Boolean":
case "Number":
case "ChooseOne":
case "ChooseOneCustom":
case "ChooseMultiple":
case "PayloadList":
case "Array":
case "TypedArray":
case "LinkInfo":
//console.log("submit param", param)
collapsedParameters[param.name] = param.value;
break;
case "AgentConnect":
if (Object.keys(param.value).length === 0){
snackActions.warning("No connection info specified")
return
}
collapsedParameters[param.name] = param.value;
break
case "File":
case "FileMultiple":
break
case "CredentialJson":
collapsedParameters[param.name] = {
account: param.value["account"],
comment: param.value["comment"],
credential: param.value["credential_text"],
realm: param.value["realm"],
type: param.value["type"]
};
break;
default:
console.log("Unknown parameter type");
}
}
return collapsedParameters;
}
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" />
@@ -970,8 +1024,9 @@ export function TaskParametersDialog(props) {
callback_id={props.callback_id}
onAgentConnectAddNewPayloadOnHost={onAgentConnectAddNewPayloadOnHost}
onAgentConnectRemovePayloadOnHost={onAgentConnectRemovePayloadOnHost}
addedCredential={addedCredential}
addedCredential={addedCredential} removedCredential={removedCredential}
setSubmenuOpenPreventTasking={setSubmenuOpenPreventTasking}
getOtherParameters={getOtherParameters}
/>
))}
</TableBody>
File diff suppressed because it is too large. Load diff
@@ -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>
@@ -1,28 +1,25 @@
import React, {useState} from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import {useQuery, gql} from '@apollo/client';
import LinearProgress from '@mui/material/LinearProgress';
import AceEditor from 'react-ace';
import 'ace-builds/src-noconflict/mode-json';
import 'ace-builds/src-noconflict/theme-monokai';
import 'ace-builds/src-noconflict/theme-xcode';
import "ace-builds/src-noconflict/ext-searchbox";
import {useTheme} from '@mui/material/styles';
import {MythicModifyStringDialog} from "../../MythicComponents/MythicDialog";
import { meState } from '../../../cache';
import {toLocalTime} from "../../utilities/Time";
const getParametersQuery = gql`
query getParametersQuery ($task_id: Int!) {
task_by_pk(id: $task_id) {
display_params
original_params
mythic_parsed_params
params
tasking_location
parameter_group_name
command_name
id
timestamp
status_timestamp_preprocessing
status_timestamp_processing
status_timestamp_processed
command {
cmd
id
@@ -37,11 +34,12 @@ query getParametersQuery ($task_id: Int!) {
export function TaskViewParametersDialog(props) {
const [comment, setComment] = useState("");
const theme = useTheme();
const { loading, error } = useQuery(getParametersQuery, {
variables: {task_id: props.task_id},
onCompleted: data => {
let workingComment = "Original Parameters:\n" + data.task_by_pk.original_params;
workingComment += "\n\nMythic Parsed Parameters: \n\tUse this for eventing (create_task->action_data->params_dictionary) and scripting (mythic.issue_task->parameters)\n"
workingComment += data.task_by_pk.mythic_parsed_params;
workingComment += "\n\nAgent Parameters:\n" + data.task_by_pk.params;
workingComment += "\n\nDisplay Parameters:\n" + data.task_by_pk.display_params;
workingComment += "\n\nTasking Location:\n" + data.task_by_pk.tasking_location;
@@ -53,6 +51,11 @@ export function TaskViewParametersDialog(props) {
}
workingComment += "\n\nPayload Type:\n" + data.task_by_pk.command.payloadtype.name;
}
workingComment += "\n\n--------IMPORTANT TIMESTAMPS--------\n\n";
workingComment += "Task Submitted by Operator : " + toLocalTime(data.task_by_pk.status_timestamp_preprocessing, meState()?.user?.view_utc_time) + "\n";
workingComment += "Task Picked up by Agent : " + toLocalTime(data.task_by_pk.status_timestamp_processing, meState()?.user?.view_utc_time) + "\n";
workingComment += "First message from Task : " + toLocalTime(data.task_by_pk.status_timestamp_processed, meState()?.user?.view_utc_time) + "\n";
workingComment += "Last message from Task : " + toLocalTime(data.task_by_pk.timestamp, meState()?.user?.view_utc_time) + "\n";
setComment(workingComment);
},
fetchPolicy: "network-only"
@@ -65,13 +68,14 @@ export function TaskViewParametersDialog(props) {
return <div>Error!</div>;
}
return (
<React.Fragment>
<MythicModifyStringDialog title={`View Task Parameters`}
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} />
</>
);
}
@@ -1,15 +1,6 @@
import React, {useState} from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import {useQuery, gql} from '@apollo/client';
import LinearProgress from '@mui/material/LinearProgress';
import AceEditor from 'react-ace';
import 'ace-builds/src-noconflict/mode-json';
import 'ace-builds/src-noconflict/theme-monokai';
import 'ace-builds/src-noconflict/theme-xcode';
import "ace-builds/src-noconflict/ext-searchbox";
import {useTheme} from '@mui/material/styles';
import {MythicModifyStringDialog} from "../../MythicComponents/MythicDialog";
@@ -25,7 +16,6 @@ query getParametersQuery ($task_id: Int!) {
export function TaskViewStdoutStderrDialog(props) {
const [comment, setComment] = useState("");
const theme = useTheme();
const { loading, error } = useQuery(getParametersQuery, {
variables: {task_id: props.task_id},
onCompleted: data => {
@@ -1,44 +0,0 @@
import React from 'react';
import { ConsumingServicesTable } from './ConsumingServicesTable';
import {useSubscription, gql } from '@apollo/client';
const sub_consuming_services = gql`
subscription ConsumingContainer{
consuming_container_stream(batch_size: 10, cursor: {initial_value: {updated_at: "1970-01-01"}}) {
id
name
description
type
container_running
deleted
subscriptions
}
}
`;
export function ConsumingServices({me}){
const [services, setServices] = React.useState([]);
useSubscription(sub_consuming_services, {
fetchPolicy: "no-cache",
onData: ({data}) => {
const updated = data.data.consuming_container_stream.reduce( (prev, cur) => {
let existingIndex = prev.findIndex( (element, i, array) => element.id === cur.id);
if(existingIndex > -1){
prev[existingIndex] = {...prev[existingIndex], ...cur};
return [...prev];
}
return [...prev, cur];
}, services);
setServices(updated);
},
onError: ({data}) => {
console.log(data)
},
});
return (
<div style={{display: "flex", flexDirection: "column", height: "100%"}}>
<ConsumingServicesTable
me={me}
services={services}
/>
</div>
);
}
@@ -1,477 +0,0 @@
import React from 'react';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import {useTheme} from '@mui/material/styles';
import PublicIcon from '@mui/icons-material/Public';
import {IconButton} from '@mui/material';
import {gql, useMutation} from '@apollo/client';
import {snackActions} from '../../utilities/Snackbar';
import SyncAltIcon from '@mui/icons-material/SyncAlt';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import MythicTableCell from "../../MythicComponents/MythicTableCell";
import DeleteIcon from '@mui/icons-material/Delete';
import RestoreFromTrashOutlinedIcon from '@mui/icons-material/RestoreFromTrashOutlined';
import {MythicConfirmDialog} from "../../MythicComponents/MythicConfirmDialog";
import PermIdentityTwoToneIcon from '@mui/icons-material/PermIdentityTwoTone';
import {MythicDialog} from "../../MythicComponents/MythicDialog";
import AttachFileIcon from '@mui/icons-material/AttachFile';
import {ConsumingServicesGetIDPMetadataDialog} from "./ConsumingServicesGetIDPMetadataDialog";
import {C2ProfileListFilesDialog} from "../PayloadTypesC2Profiles/C2ProfileListFilesDialog";
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import VisibilityIcon from '@mui/icons-material/Visibility';
const testWebhookMutation = gql`
mutation testWebhookWorks($service_type: String!){
consumingServicesTestWebhook(service_type: $service_type){
status
error
}
}
`;
const testLogMutation = gql`
mutation testWebhookWorks($service_type: String!){
consumingServicesTestLog(service_type: $service_type){
status
error
}
}
`;
const toggleDeleteStatus = gql`
mutation toggleConsumingContainerDeleteStatus($id: Int!, $deleted: Boolean!){
update_consuming_container_by_pk(pk_columns: {id: $id}, _set: {deleted: $deleted}) {
id
}
}
`;
const webhook_events = ["new_alert","new_callback","new_custom","new_feedback", "new_startup"];
const logging_events = ["new_artifact","new_callback", "new_credential","new_file", "new_keylog", "new_payload", "new_response", "new_task"];
export function ConsumingServicesTable({services}) {
const theme = useTheme();
const [showDeleted, setShowDeleted] = React.useState(false);
const [testWebhook] = useMutation(testWebhookMutation, {
onCompleted: data => {
if (data.consumingServicesTestWebhook.status === "success") {
snackActions.success("Successfully sent test message to service");
} else {
console.log(data.consumingServicesTestWebhook.error)
snackActions.error("No webhook listening")
}
},
onError: error => {
}
});
const issueTestWebhook = (service_type) => {
testWebhook({variables: {service_type: service_type}});
}
const [testLog] = useMutation(testLogMutation, {
onCompleted: data => {
if (data.consumingServicesTestLog.status === "success") {
snackActions.success("Successfully sent test message to service");
} else {
snackActions.error("No logger listening")
console.log(data.consumingServicesTestLog.error)
}
},
onError: error => {
}
});
const issueTestLog = (service_type) => {
testLog({variables: {service_type: service_type}});
}
const [openIDPMetadata, setOpenIDPMetadata] = React.useState(false);
const IDPMetadataRef = React.useRef({"container": "", "idp": ""});
const openListFilesInformation = React.useRef("");
const [webhooks, setWebhooks] = React.useState([]);
const [logging, setLogging] = React.useState([]);
const [eventing, setEventing] = React.useState([]);
const [auth, setAuth] = React.useState([]);
const [openListFilesDialog, setOpenListFilesDialog] = React.useState(false);
const [openDelete, setOpenDeleteDialog] = React.useState(false);
const deletingContainer = React.useRef({});
const [updateDeleted] = useMutation(toggleDeleteStatus, {
onCompleted: data => {
snackActions.success("Successfully updated");
},
onError: error => {
console.log(error);
snackActions.error("Failed to update: " + error.message);
}
});
const onAcceptDelete = () => {
updateDeleted({variables: {id: deletingContainer.current.id, deleted: !deletingContainer.current.deleted}})
setOpenDeleteDialog(false);
}
const adjustingDelete = (service) => {
deletingContainer.current = service;
setOpenDeleteDialog(true);
}
const getIDPMetadata = (container, idp) => {
IDPMetadataRef.current = {"container": container, "idp": idp};
setOpenIDPMetadata(true);
}
const onOpenListFilesDialog = (container) => {
openListFilesInformation.current = container;
setOpenListFilesDialog(true);
}
React.useEffect(() => {
const localWebhooks = services.filter(cur => cur.type === "webhook").sort((a, b) => a.name < b.name ? 1 : -1);
setWebhooks(localWebhooks);
const localLogging = services.filter(cur => cur.type === "logging").sort((a, b) => a.name < b.name ? 1 : -1);
setLogging(localLogging);
const localEventing = services.filter(cur => cur.type === "eventing").sort((a, b) => a.name < b.name ? 1 : -1);
const parsedLocalEventing = localEventing.map( e => {
const newSubs = e.subscriptions.map( s => {
try{
return JSON.parse(s);
}catch(error){
console.log(error);
return {name: "", description: s};
}
});
return {...e, subscriptions: newSubs}
})
setEventing(parsedLocalEventing);
const localAuth = services.filter(cur => cur.type === "auth").sort((a, b) => a.name < b.name ? 1 : -1);
const parsedLocalAuth = localAuth.map( e => {
const newSubs = e.subscriptions.map( s => {
try{
return JSON.parse(s);
}catch(error){
console.log(error);
return {name: s, type: ""};
}
});
return {...e, subscriptions: newSubs}
})
setAuth(parsedLocalAuth);
}, [services]);
return (
<React.Fragment>
<Paper elevation={5} style={{
backgroundColor: theme.pageHeader.main,
color: theme.pageHeaderText.main,
marginBottom: "5px",
marginLeft: "5px",
marginRight: "5px"
}} variant={"elevation"}>
<Typography variant="h3" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
Containers Consuming Events
</Typography>
{showDeleted ? (
<MythicStyledTooltip title={"Hide Deleted Services"} style={{float: "right"}}>
<IconButton size="small" style={{float: "right", marginTop: "5px"}} variant="contained" onClick={() => setShowDeleted(!showDeleted)}><VisibilityIcon /></IconButton>
</MythicStyledTooltip>
) : (
<MythicStyledTooltip title={"Show Deleted Services"} style={{float: "right"}}>
<IconButton size="small" style={{float: "right", marginTop: "5px"}} variant="contained" onClick={() => setShowDeleted(!showDeleted)} ><VisibilityOffIcon /></IconButton>
</MythicStyledTooltip>
)}
</Paper>
<div style={{display: "flex", flexGrow: 1}}>
<TableContainer className="mythicElement">
<Table stickyHeader={true} size="small" style={{"tableLayout": "fixed", "overflow": "scroll"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "3rem"}}></TableCell>
<TableCell style={{width: "10rem"}}>Name</TableCell>
<TableCell style={{width: "7rem"}}>Category</TableCell>
<TableCell>Description</TableCell>
<TableCell style={{width: "4rem"}}>Manage</TableCell>
<TableCell>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{webhooks.map( (w, index) => (
(showDeleted || !w.deleted) &&
<TableRow key={w.id} hover>
<MythicTableCell>
{w.deleted ? (
<IconButton onClick={() => {
adjustingDelete(w);
}} color="success" size="large">
<RestoreFromTrashOutlinedIcon/>
</IconButton>
) : (
<IconButton onClick={() => {
adjustingDelete(w);
}} color="error" size="large">
<DeleteIcon/>
</IconButton>
)}
</MythicTableCell>
<MythicTableCell>
{w.name}
{w.container_running &&
<Typography variant="body2" component="p"
style={{color: theme.palette.success.main}}>
<b>{"Online"}</b>
</Typography>
}
{!w.container_running &&
<Typography variant="body2" component="p"
style={{color: theme.palette.error.main}}>
<b>{"Offline"}</b>
</Typography>
}
</MythicTableCell>
<MythicTableCell>{w.type}</MythicTableCell>
<MythicTableCell>{w.description}</MythicTableCell>
<MythicTableCell>
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
<IconButton
color={"secondary"}
disabled={!w.container_running}
onClick={()=>{onOpenListFilesDialog(w.name);}}
size="large">
<AttachFileIcon />
</IconButton>
</MythicStyledTooltip>
</MythicTableCell>
<MythicTableCell>
{webhook_events.map(s => (
<MythicStyledTooltip title={"test webhook " + s} key={w.id + "webhook_" + s}>
<IconButton
disabled={!w.subscriptions.includes(s)}
onClick={() => {
issueTestWebhook(s)
}}
size="large">
<PublicIcon/>
</IconButton>
</MythicStyledTooltip>
))}
</MythicTableCell>
</TableRow>
))}
{logging.map(w => (
(showDeleted || !w.deleted) &&
<TableRow key={w.id} hover>
<MythicTableCell>
{w.deleted ? (
<IconButton onClick={() => {
adjustingDelete(w);
}} color="success" size="large">
<RestoreFromTrashOutlinedIcon/>
</IconButton>
) : (
<IconButton onClick={() => {
adjustingDelete(w);
}} color="error" size="large">
<DeleteIcon/>
</IconButton>
)}
</MythicTableCell>
<MythicTableCell>
{w.name}
{w.container_running &&
<Typography variant="body2" component="p"
style={{color: theme.palette.success.main}}>
<b>{"Online"}</b>
</Typography>
}
{!w.container_running &&
<Typography variant="body2" component="p"
style={{color: theme.palette.error.main}}>
<b>{"Offline"}</b>
</Typography>
}
</MythicTableCell>
<MythicTableCell>{w.type}</MythicTableCell>
<MythicTableCell>{w.description}</MythicTableCell>
<MythicTableCell>
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
<IconButton
color={"secondary"}
disabled={!w.container_running}
onClick={()=>{onOpenListFilesDialog(w.name);}}
size="large">
<AttachFileIcon />
</IconButton>
</MythicStyledTooltip>
</MythicTableCell>
<MythicTableCell>
{logging_events.map(s => (
<MythicStyledTooltip title={"test logging " + s} key={w.id + "logging_" + s}>
<IconButton
disabled={!w.subscriptions.includes(s)}
onClick={() => {
issueTestLog(s)
}}
size="large">
<SyncAltIcon/>
</IconButton>
</MythicStyledTooltip>
))}
</MythicTableCell>
</TableRow>
))}
{eventing.map( (w, index) => (
(showDeleted || !w.deleted) &&
<TableRow key={w.id} hover>
<MythicTableCell>
{w.deleted ? (
<IconButton onClick={() => {
adjustingDelete(w);
}} color="success" size="large">
<RestoreFromTrashOutlinedIcon/>
</IconButton>
) : (
<IconButton onClick={() => {
adjustingDelete(w);
}} color="error" size="large">
<DeleteIcon/>
</IconButton>
)}
</MythicTableCell>
<MythicTableCell>
{w.name}
{w.container_running &&
<Typography variant="body2" component="p"
style={{color: theme.palette.success.main}}>
<b>{"Online"}</b>
</Typography>
}
{!w.container_running &&
<Typography variant="body2" component="p"
style={{color: theme.palette.error.main}}>
<b>{"Offline"}</b>
</Typography>
}
</MythicTableCell>
<MythicTableCell>{w.type}</MythicTableCell>
<MythicTableCell>{w.description}</MythicTableCell>
<MythicTableCell>
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
<IconButton
color={"secondary"}
disabled={!w.container_running}
onClick={()=>{onOpenListFilesDialog(w.name);}}
size="large">
<AttachFileIcon />
</IconButton>
</MythicStyledTooltip>
</MythicTableCell>
<MythicTableCell>
<Table>
<TableHead>
<TableRow>
<TableCell>Function</TableCell>
<TableCell>Description</TableCell>
</TableRow>
</TableHead>
<TableBody>
{w.subscriptions.map(s => (
<TableRow key={s.name} hover>
<MythicTableCell><b>{s.name}</b></MythicTableCell>
<MythicTableCell>{s.description}</MythicTableCell>
</TableRow>
))}
</TableBody>
</Table>
</MythicTableCell>
</TableRow>
))}
{auth.map( (w, index) => (
(showDeleted || !w.deleted) &&
<TableRow key={w.id} hover>
<MythicTableCell>
{w.deleted ? (
<IconButton onClick={() => {
adjustingDelete(w);
}} color="success" size="large">
<RestoreFromTrashOutlinedIcon/>
</IconButton>
) : (
<IconButton onClick={() => {
adjustingDelete(w);
}} color="error" size="large">
<DeleteIcon/>
</IconButton>
)}
</MythicTableCell>
<MythicTableCell>
{w.name}
{w.container_running &&
<Typography variant="body2" component="p"
style={{color: theme.palette.success.main}}>
<b>{"Online"}</b>
</Typography>
}
{!w.container_running &&
<Typography variant="body2" component="p"
style={{color: theme.palette.error.main}}>
<b>{"Offline"}</b>
</Typography>
}
</MythicTableCell>
<MythicTableCell>{w.type}</MythicTableCell>
<MythicTableCell>{w.description}</MythicTableCell>
<MythicTableCell>
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
<IconButton
color={"secondary"}
disabled={!w.container_running}
onClick={()=>{onOpenListFilesDialog(w.name);}}
size="large">
<AttachFileIcon />
</IconButton>
</MythicStyledTooltip>
</MythicTableCell>
<MythicTableCell>
{w.subscriptions.map(s => (
<Typography key={s.name + s.type + w.name} style={{display: "block"}}>
<MythicStyledTooltip title={"Fetch Container Metadata"} >
<IconButton onClick={() => getIDPMetadata(w.name, s.name)} >
<PermIdentityTwoToneIcon />
</IconButton>
</MythicStyledTooltip>
{s.name}
</Typography>
))}
</MythicTableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
{openDelete &&
<MythicConfirmDialog onClose={() => { setOpenDeleteDialog(false); }}
onSubmit={onAcceptDelete}
open={openDelete}
acceptText={deletingContainer.current.deleted ? "Restore" : "Remove"}
acceptColor={deletingContainer.current.deleted ? "success" : "error"}/>
}
{openIDPMetadata &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openIDPMetadata}
onClose={()=>{setOpenIDPMetadata(false);}}
innerDialog={<ConsumingServicesGetIDPMetadataDialog
container={IDPMetadataRef.current.container}
idp={IDPMetadataRef.current.idp}
onClose={()=>{setOpenIDPMetadata(false);}} />}
/>
}
{openListFilesDialog &&
<MythicDialog fullWidth={true} maxWidth="md" open={openListFilesDialog}
onClose={()=>{setOpenListFilesDialog(false);}}
innerDialog={<C2ProfileListFilesDialog container_name={openListFilesInformation.current}
onClose={()=>{setOpenListFilesDialog(false);}} />}
/>
}
</div>
</React.Fragment>
);
}
@@ -3,14 +3,14 @@ import Stepper from '@mui/material/Stepper';
import Step from '@mui/material/Step';
import StepLabel from '@mui/material/StepLabel';
import {Step1SelectOS} from './Step1SelectOS';
import {Step2SelectPayloadType} from './Step2SelectPayloadType';
import {Step3SelectCommands} from './Step3SelectCommands';
import {Step4C2Profiles} from './Step4C2Profiles';
import {Step5Build} from './Step5Build';
import Typography from '@mui/material/Typography';
import { snackActions } from '../../utilities/Snackbar';
function getSteps(){
return ['Select Target OS', 'Payload Type', 'Select Commands', 'Select C2 Profiles', 'Build']
return ['Select Target OS', 'Configure Payload', 'Select Commands', 'Select C2 Profiles', 'Build']
}
export function CreatePayload(props){
@@ -21,21 +21,30 @@ export function CreatePayload(props){
const getStepContent = (step) => {
switch (step) {
case 0:
return <Step1SelectOS buildOptions={payload} prevData={payload[0]} finished={handleStepData} canceled={cancelStep} first={true} last={false}/>;
return <Step1SelectOS buildOptions={payload} prevData={payload[0]} setAllData={setAllData} finished={handleStepData} canceled={cancelStep} first={true} last={false}/>;
case 1:
return <Step2SelectPayloadType buildOptions={payload[0]} prevData={payload[1]} finished={handleStepData} canceled={cancelStep} first={false} last={false}/>;
return <Step1SelectOS buildOptions={payload} prevData={payload[1]} finished={handleStepData} canceled={cancelStep} first={false} last={false}/>;
case 2:
return <Step3SelectCommands buildOptions={payload[1]} prevData={payload[2]} finished={handleStepData} canceled={cancelStep} first={false} last={false} />;
case 3:
return <Step4C2Profiles buildOptions={payload[1]} prevData={payload[3]} finished={handleStepData} canceled={cancelStep} first={false} last={false} />;
case 4:
return <Step5Build me={me} buildOptions={payload} canceled={cancelStep} first={false} last={true} startOver={startOver} />;
return <Step5Build me={me} buildOptions={payload} moveToStep={moveToStep} canceled={cancelStep} first={false} last={true} startOver={startOver} />;
default:
return 'Unknown step';
}
}
const handleStepData = (stepData) => {
setPayload({...payload, [activeStep]: stepData});
const setAllData = (stepData) => {
setPayload(stepData);
handleNext();
}
const handleStepData = (stepData, clearNextPreviousData) => {
let newPayload = {...payload, [activeStep]: stepData};
if(clearNextPreviousData){
newPayload[activeStep+1] = undefined;
}
setPayload(newPayload);
//console.log(activeStep, newPayload);
handleNext();
}
const cancelStep = () => {
@@ -53,6 +62,9 @@ export function CreatePayload(props){
const startOver = () => {
setActiveStep(0);
}
const moveToStep = (step) => {
setActiveStep(step);
}
React.useEffect( () => {
if(noOperation){
snackActions.error("No current operation set! Set a current operation to continue");
@@ -60,34 +72,40 @@ export function CreatePayload(props){
}, [noOperation])
return (
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%"}} >
<Stepper activeStep={activeStep} alternativeLabel style={{marginTop: "10px"}}>
{steps.map((label, index) => (
<Step key={label}
sx={{
'& .MuiStepLabel-root .Mui-completed': {
color: 'success.main', // circle color (COMPLETED)
},
'& .MuiStepLabel-label.Mui-completed.MuiStepLabel-alternativeLabel':
{
color: 'grey.500', // Just text label (COMPLETED)
},
'& .MuiStepLabel-root .Mui-active': {
color: 'info.main', // circle color (ACTIVE)
},
'& .MuiStepLabel-label.Mui-active.MuiStepLabel-alternativeLabel':
{
fontWeight: "bold", // Just text label (ACTIVE)
color: ''
},
'& .MuiStepLabel-root .Mui-active .MuiStepIcon-text': {
fill: 'black', // circle's number (ACTIVE)
},
}}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%"}}>
<div style={{display: "flex", flexDirection: "row", width: "100%", alignItems: "center"}}>
<Typography variant="h5" style={{marginLeft: "10px", width: "25%"}}>
Payload Creation
</Typography>
<Stepper activeStep={activeStep} alternativeLabel style={{marginTop: "10px", width: "100%"}}>
{steps.map((label, index) => (
<Step key={label}
sx={{
'& .MuiStepLabel-root .Mui-completed': {
color: 'success.main', // circle color (COMPLETED)
},
'& .MuiStepLabel-label.Mui-completed.MuiStepLabel-alternativeLabel':
{
color: 'grey.500', // Just text label (COMPLETED)
},
'& .MuiStepLabel-root .Mui-active': {
color: 'info.main', // circle color (ACTIVE)
},
'& .MuiStepLabel-label.Mui-active.MuiStepLabel-alternativeLabel':
{
fontWeight: "bold", // Just text label (ACTIVE)
color: ''
},
'& .MuiStepLabel-root .Mui-active .MuiStepIcon-text': {
fill: 'black', // circle's number (ACTIVE)
},
}}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
</div>
<div style={{display: "flex", flexDirection: "column", flexGrow: 1, overflowY: 'auto'}}>
{getStepContent(activeStep)}
</div>
@@ -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>
);
}
@@ -1,11 +1,10 @@
import React from 'react';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import { CreatePayloadParameter } from './CreatePayloadParameter';
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
export function CreatePayloadC2ProfileParametersTable(props){
@@ -14,13 +13,11 @@ export function CreatePayloadC2ProfileParametersTable(props){
props.onChange(props.name, paramName, value, error);
}
return (
<TableContainer >
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "auto"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "20%"}}>Parameter</TableCell>
<TableCell>Value</TableCell>
<MythicStyledTableCell style={{width: "20%"}}>Parameter</MythicStyledTableCell>
<MythicStyledTableCell>Value</MythicStyledTableCell>
</TableRow>
</TableHead>
<TableBody>
@@ -29,6 +26,5 @@ export function CreatePayloadC2ProfileParametersTable(props){
))}
</TableBody>
</Table>
</TableContainer>
);
}
@@ -14,11 +14,13 @@ export function CreatePayloadNavigationButtons(props){
const disabledButtons = (me?.user?.current_operation_id || 0) <= 0;
return (
<div >
<div style={{marginLeft: "20px"}}>
<Button
disabled={props.first}
color="primary"
color="info"
variant={"outlined"}
onClick={props.canceled}
style={{marginRight: "10px"}}
>
Back
</Button>
@@ -2,7 +2,6 @@ import React, {useEffect} from 'react';
import Table from '@mui/material/Table';
import TableContainer from '@mui/material/TableContainer';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableRow from '@mui/material/TableRow';
import FormControl from '@mui/material/FormControl';
import Select from '@mui/material/Select';
@@ -20,39 +19,191 @@ import MythicStyledTableCell from '../../MythicComponents/MythicTableCell';
import {Typography} from '@mui/material';
import {MythicFileContext} from "../../MythicComponents/MythicFileContext";
import {snackActions} from "../../utilities/Snackbar";
import {useTheme} from '@mui/material/styles';
import {gql, useMutation} from '@apollo/client';
import { Backdrop } from '@mui/material';
import {CircularProgress} from '@mui/material';
import RefreshIcon from '@mui/icons-material/Refresh';
import InputLabel from '@mui/material/InputLabel';
import {DragAndDropFileUpload} from "../Callbacks/TaskParametersDialogRow";
export function CreatePayloadParameter({onChange, parameter_type, default_value, name, required, verifier_regex, id, description, initialValue, choices, trackedValue, returnAllDictValues}){
export const getDynamicQueryBuildParameterParams = gql`
mutation getDynamicBuildParamsMutation($payload_type: String!, $parameter_name: String!, $selected_os: String!){
dynamicQueryBuildParameterFunction(payload_type: $payload_type, parameter_name: $parameter_name, selected_os: $selected_os){
status
error
choices
parameter_name
}
}
`;
function isTrue(value){
if(typeof value === 'boolean'){
return value;
}
if(typeof value === 'string'){
return value.toLowerCase() === 'true' || value.toLowerCase() === 't';
}
console.log("unknown boolean value", value);
}
export function CreatePayloadParameter({onChange, parameter_type, default_value, name, required, verifier_regex, id,
description, initialValue, choices, trackedValue, instance_name,
payload_type, selected_os, dynamic_query_function}){
const theme = useTheme();
const [value, setValue] = React.useState("");
const [valueNum, setValueNum] = React.useState(0);
const [multiValue, setMultiValue] = React.useState([]);
const [ChoiceOptions, setChoiceOptions] = React.useState([]);
const [chooseOneCustomValue, setChooseOneCustomValue] = React.useState("");
const [dictValue, setDictValue] = React.useState([]);
const [dictOptions, setDictOptions] = React.useState([]);
const [dictSelectOptions, setDictSelectOptions] = React.useState([]);
const [dictSelectOptionsChoice, setDictSelectOptionsChoice] = React.useState("");
const [chooseOptions, setChooseOptions] = React.useState([]);
const [dateValue, setDateValue] = React.useState(dayjs(new Date()));
const [arrayValue, setArrayValue] = React.useState([""]);
const [arrayValue, setArrayValue] = React.useState([]);
const [typedArrayValue, setTypedArrayValue] = React.useState([]);
const [fileValue, setFileValue] = React.useState({name: ""});
const [fileMultValue, setFileMultValue] = React.useState([]);
const [mapArray, setMapArray] = React.useState([]);
const [backdropOpen, setBackdropOpen] = React.useState(false);
const usingDynamicParamChoices = React.useRef(false);
const [getDynamicParams] = useMutation(getDynamicQueryBuildParameterParams, {
onCompleted: (data) => {
if(data.dynamicQueryBuildParameterFunction.status === "success"){
try{
let choicesInUse = [];
usingDynamicParamChoices.current = true;
setChoiceOptions([...data.dynamicQueryBuildParameterFunction.choices]);
choicesInUse = [...data.dynamicQueryBuildParameterFunction.choices];
if(parameter_type === "ChooseOne"){
if(choicesInUse.length > 0){
if(value !== "") {
setValue(value);
onChange(name, value, false);
} else if(choicesInUse.includes(default_value)) {
setValue(default_value);
onChange(name, default_value, false);
} else {
setValue(choicesInUse[0]);
onChange(name, choicesInUse[0], false);
}
}
} else if(parameter_type === "ChooseOneCustom"){
let newStandardValue = default_value;
if(choicesInUse.includes(default_value) && value !== "") {
setValue(default_value);
} else {
setValue(choicesInUse[0]);
newStandardValue = choicesInUse[0];
}
if(!choicesInUse.includes(value) && value !== "" ){
setChooseOneCustomValue(value);
newStandardValue = value;
}
onChange(name, newStandardValue, false);
} else if(parameter_type === "ChooseMultiple"){
if(choicesInUse.length > 0){
if(multiValue.length > 0) {
setMultiValue(multiValue);
onChange(name, multiValue, false);
} else if(choicesInUse.includes(default_value)) {
setMultiValue(default_value);
onChange(name, default_value, false);
} else {
setMultiValue([choicesInUse[0]]);
onChange(name, [choicesInUse[0]], false);
}
}
}
}catch(error){
setBackdropOpen(false);
snackActions.warning("Failed to parse dynamic parameter results");
}
}else{
snackActions.warning(data.dynamicQueryBuildParameterFunction.error);
}
setBackdropOpen(false);
},
onError: (data) => {
snackActions.warning("Failed to perform dynamic parameter query");
console.log(data);
setBackdropOpen(false);
}
});
const reIssueDynamicQueryFunction = () => {
setBackdropOpen(true);
snackActions.info("Querying payload type container for options...", {autoClose: 1000});
getDynamicParams({variables:{
parameter_name: name,
selected_os: selected_os,
payload_type: payload_type,
}})
usingDynamicParamChoices.current = true;
}
const arrayMapToMap = (val) => {
return val.reduce( ( prev, cur ) => {
return {...prev, [cur[0]]: cur[1]}
}, {})
}
const addMapArrayMap = () => {
let map = [...mapArray, ["", []]];
setMapArray(map);
onChange(name, arrayMapToMap(map), false);
}
const updateMapArrayMap = (i, val) => {
let map = [...mapArray];
map[i][0] = val;
setMapArray(map);
onChange(name, arrayMapToMap(map), false);
}
const removeMapArrayMap = (i) => {
let map = mapArray.toSpliced(i, 1)
setMapArray(map);
onChange(name, arrayMapToMap(map), false);
}
const addMapArray = (i) => {
let map = [...mapArray];
map[i][1].push("");
setMapArray(map);
onChange(name, arrayMapToMap(map), false);
}
const updateMapArray = (i, j, val) => {
let map = [...mapArray];
map[i][1][j] = val;
setMapArray(map);
onChange(name, arrayMapToMap(map), false);
}
const removeMapArray = (i, j, val) => {
let map = [...mapArray];
map[i][1].splice(j, 1);
setMapArray(map);
onChange(name, arrayMapToMap(map), false);
}
const submitDictChange = (list) => {
onChange(name, list, false);
};
const onFileChange = (evt) => {
setFileValue({name: evt.target.files[0].name});
onChange(name, evt.target.files[0]);
const onFileChange = (newFile) => {
setFileValue({name: newFile.name});
onChange(name, newFile);
}
const onFileMultChange = (evt) => {
setFileMultValue([...evt.target.files]);
onChange(name, [...evt.target.files]);
const onFileMultChange = (newFiles) => {
setFileMultValue([...newFiles]);
onChange(name, [...newFiles]);
}
useEffect( () => {
if(parameter_type === "ChooseOne" || parameter_type === "ChooseOneCustom"){
setValue(trackedValue);
setChooseOptions(choices);
if( parameter_type === "ChooseOne" || parameter_type === "ChooseOneCustom" ){
setChoiceOptions(choices);
if(!choices.includes(trackedValue)){
setChooseOneCustomValue(trackedValue);
if(dynamic_query_function !== ""){
setValue(trackedValue);
} else {
setValue(default_value);
}
} else {
setValue(trackedValue);
setChooseOneCustomValue("");
}
}else if(parameter_type === "Number"){
setValueNum(trackedValue);
@@ -60,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 !== ""});
@@ -109,12 +260,30 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
setValue( trackedValue );
}else if(parameter_type === "Array") {
setArrayValue(trackedValue);
}else if(parameter_type === "TypedArray"){
}else if(parameter_type === "TypedArray") {
setTypedArrayValue(trackedValue);
}else if(parameter_type === "MapArray") {
let initial = [];
for(const [key, val] of Object.entries(trackedValue)){
initial.push([key, val]);
}
setMapArray(initial);
}else{
console.log("hit an unknown parameter type")
}
}, [default_value, parameter_type, name]);
if(dynamic_query_function !== "" && dynamic_query_function !== undefined){
if(!usingDynamicParamChoices.current){
setBackdropOpen(true);
snackActions.info("Querying payload type container for options...", {autoClose: 1000});
getDynamicParams({variables:{
parameter_name: name,
payload_type: payload_type,
selected_os: selected_os
}})
}
usingDynamicParamChoices.current = true;
}
}, [default_value, parameter_type, name, instance_name]);
const onChangeTextChooseOneCustom = (name, newValue, error) => {
setChooseOneCustomValue(newValue);
if(newValue === ""){
@@ -129,15 +298,9 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
onChange(name, evt.target.value, false);
}
const onChangeMultValue = (evt) => {
const {options} = evt.target;
const tmpValue = [];
for (let i = 0, l = options.length; i < l; i += 1) {
if (options[i].selected) {
tmpValue.push(options[i].value);
}
}
setMultiValue(tmpValue);
onChange(name, tmpValue, false);
const { value:options } = evt.target;
setMultiValue(options);
onChange(name, options, false);
}
const onChangeText = (name, value, error) => {
setValue(value);
@@ -210,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);
@@ -260,20 +423,7 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
}
const onChangeArrayText = (value, error, index) => {
let values = [...arrayValue];
if(value.includes("\n")){
let new_values = value.split("\n");
if(values[index] === ""){
values = [...values.slice(0, index+1), ...new_values.slice(1), ...values.slice(index+1)];
values[index] = values[index] + new_values[0];
} else if(values[index] === new_values[0]){
values = [...values.slice(0, index+1), ...new_values.slice(1), ...values.slice(index+1)];
} else {
new_values[0] = new_values[0].slice(values[index].length)
values = [...values.slice(0, index+1), ...new_values, ...values.slice(index+1)];
}
}else{
values[index] = value;
}
values[index] = value;
setArrayValue(values);
onChange(name, values, false);
}
@@ -325,55 +475,55 @@ 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}>
{typeof f === "string" && <MythicFileContext agent_file_id={f} />}
{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} />
}
<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={{}}>
<FormControl style={{width: "20%"}}>
<Select
native
multiple={false}
disabled={chooseOneCustomValue !== ""}
value={value}
@@ -381,36 +531,58 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
input={<Input />}
>
{
chooseOptions.map((opt, i) => (
<option key={name + i} value={opt}>{opt}</option>
ChoiceOptions.map((opt, i) => (
<MenuItem key={name + i} value={opt}>{opt}</MenuItem>
))
}
</Select>
</FormControl>
OR
<MythicTextField name={name} required={required} placeholder={"Custom Value"} value={chooseOneCustomValue} multiline={true} maxRows={5}
<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}
native
onChange={onChangeMultValue}
>
{
chooseOptions.map((opt, i) => (
<option key={"buildparamopt" + i} value={opt}>{opt}</option>
))
<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 (
@@ -420,14 +592,14 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
{arrayValue.map( (a, i) => (
<TableRow key={'array' + name + i} style={{}} >
<MythicStyledTableCell style={{width: "2rem"}}>
<DeleteIcon onClick={(e) => {removeArrayValue(i)}} color="error"
style={{cursor: "pointer"}}
/>
<IconButton onClick={(e) => {removeArrayValue(i)}} color="error">
<DeleteIcon/>
</IconButton>
</MythicStyledTableCell>
<MythicStyledTableCell>
<MythicTextField required={required} fullWidth={true} placeholder={""} value={a} multiline={true}
onChange={(n,v,e) => onChangeArrayText(v, e, i)} display="inline-block" autoFocus={a === ""}
validate={testParameterValues} errorText={"Must match: " + verifier_regex}
<MythicTextField requiredValue={required} fullWidth={true} placeholder={""} value={a} multiline={true}
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>
</TableRow>
@@ -456,7 +628,6 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
<div style={{display: "inline-flex", alignItems: "center", width: "100%"}}>
<FormControl style={{width: "30%"}}>
<Select
value={a[0]}
onChange={(e) => onChangeTypedArrayChoice(e, i)}
input={<Input />}
@@ -521,19 +692,74 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
);
case "Number":
return (
<MythicTextField required={required} value={valueNum} type={"number"}
<MythicTextField requiredValue={required} value={valueNum} type={"number"}
onChange={onChangeNumber} display="inline-block" name={name} showLabel={false}
validate={testParameterValues} errorText={"Must match: " + verifier_regex}
marginBottom={"0px"}
/>
);
case "Boolean":
return (
<Switch
checked={Boolean(value)}
color={"info"}
checked={isTrue(value)}
onChange={toggleSwitchValue}
inputProps={{ 'aria-label': 'primary checkbox' }}
inputProps={{ 'aria-label': 'info checkbox' }}
/>
);
case "MapArray":
return (
<>
<Table size="small" style={{tableLayout: "fixed", maxWidth: "100%", height: "100%", "overflow": "auto"}}>
<TableBody>
{mapArray.map( (val, i) => (
<TableRow key={"payloadtype" + i}>
<MythicStyledTableCell style={{width: "2rem"}}>
<IconButton onClick={(e) => {removeMapArrayMap(i)}} color="error">
<DeleteIcon/>
</IconButton>
</MythicStyledTableCell>
<MythicStyledTableCell>
<MythicTextField autoFocus={true} name={"Payload Type Name"} onChange={(name, value) => updateMapArrayMap(i, value)} value={val[0]} />
</MythicStyledTableCell>
<MythicStyledTableCell>
<Table size="small" style={{tableLayout: "fixed", maxWidth: "100%", height: "100%", "overflow": "auto"}}>
<TableBody>
{val[1].map( (v, j) => (
<TableRow key={"payloadtypevalue" + i + j}>
<MythicStyledTableCell style={{width: "2rem"}}>
<IconButton onClick={(e) => {removeMapArray(i, j)}} color="error">
<DeleteIcon/>
</IconButton>
</MythicStyledTableCell>
<MythicStyledTableCell>
<MythicTextField autoFocus={true} name={"Command Name"} onChange={(name, value) => updateMapArray(i, j, value)} value={v}
onEnter={() => addMapArray(i)}/>
</MythicStyledTableCell>
</TableRow>
))}
<TableRow>
<MythicStyledTableCell style={{width: "3rem"}}>
<IconButton onClick={() => addMapArray(i)} size="large"> <AddCircleIcon color="success" /> </IconButton>
</MythicStyledTableCell>
<MythicStyledTableCell/>
</TableRow>
</TableBody>
</Table>
</MythicStyledTableCell>
</TableRow>
))}
<TableRow>
<MythicStyledTableCell style={{width: "3rem"}}>
<IconButton onClick={addMapArrayMap} size="large"> <AddCircleIcon color="success" /> </IconButton>
</MythicStyledTableCell>
<MythicStyledTableCell/>
<MythicStyledTableCell/>
</TableRow>
</TableBody>
</Table>
</>
)
default:
return null
}
@@ -544,6 +770,8 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
return (new Date(dateValue)).toISOString().slice(0,10) !== initialValue;
case "ChooseOne":
return value !== initialValue;
case "ChooseOneCustom":
return value !== initialValue || chooseOneCustomValue !== "";
case "ChooseMultiple":
return initialValue === undefined ? false : multiValue.toString() !== initialValue.toString();
case "Array":
@@ -570,15 +798,21 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
}
return (
<TableRow key={"buildparam" + id}>
<TableCell>
<TableRow key={"buildparam" + id} hover>
<MythicStyledTableCell>
<MythicStyledTooltip title={name.length > 0 ? name : "No Description"}>
{description}
<Typography style={{fontWeight: "600"}} >
{name}
</Typography>
<Typography style={{fontSize: theme.typography.pxToRem(15), marginLeft: "10px"}}>
{description}
</Typography>
{modifiedValue() ? (
<Typography color="warning.main">Modified</Typography>
) : null}
</MythicStyledTooltip>
</TableCell>
</MythicStyledTableCell>
<MythicStyledTableCell>
{getParameterObject()}
</MythicStyledTableCell>
@@ -106,7 +106,7 @@ subscription NewPayloadsSubscription($fromNow: timestamp!) {
const SnackMessage = (props) => {
return (
<React.Fragment>
<div style={{display: "flex", flexDirection: "column"}}>
<Typography variant="subtitle2" className={classes.typography}>
{props.payloadData.build_phase === "success" ? (
"Payload successfully built!"
@@ -114,17 +114,17 @@ const SnackMessage = (props) => {
"Payload Building..."
)}
</Typography>
<PayloadsTableRowBuildProgress {...props.payloadData} />
{props.payloadData.build_phase === "success" &&
<React.Fragment>
<Typography gutterBottom>Agent ready for download</Typography>
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank" href={"/direct/download/" + props.file_id}>
Download here
</Link>
</React.Fragment>
}
</React.Fragment>
</Typography>
<PayloadsTableRowBuildProgress {...props.payloadData} />
{props.payloadData.build_phase === "success" &&
<React.Fragment>
<Typography gutterBottom>Agent ready for download</Typography>
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank" href={"/direct/download/" + props.file_id}>
Download here
</Link>
</React.Fragment>
}
</div>
);
};
@@ -208,7 +208,7 @@ export function PayloadSubscriptionNotification(props) {
setDisplayErrorDialog(true);
} else if(displayingToast.current) {
snackActions.update(getSnackMessage(), payloadData.uuid, {
type: payloadData.build_phase === "success" ? toast.TYPE.SUCCESS : toast.TYPE.INFO,
type: payloadData.build_phase === "success" ? "success" : "info",
});
}
@@ -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,28 +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
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 !== ""){
@@ -69,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();
@@ -85,144 +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 [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);
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);
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,
"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);
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!) {
@@ -29,18 +31,20 @@ query getCommands($payloadType: String!) {
`;
export function Step3SelectCommands(props){
const [commands, setCommands] = React.useState([]);
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 ) => {
if(!props.buildOptions["supports_dynamic_loading"]){
const allCommands = data.command.map( c => {
return {...c, selected: true, disabled: true, reason: "Always included because agent doesn't support dynamic loading"}
});
setCommandOptions(allCommands);
}else{
return
}
const allCommands = data.command.reduce( (prev, cur) => {
try{
const attributes = cur.attributes;
@@ -50,7 +54,9 @@ export function Step3SelectCommands(props){
}else{
try{
if(attributes["load_only"] !== undefined && attributes["load_only"]){
return [...prev, {...cur, disabled: true, selected: false, reason: "This command can only be loaded in once a callback is established"}]
// command can't be loaded here, don't bother showing it
// {...cur, disabled: true, selected: false, reason: "This command can only be loaded in once a callback is established"}
return [...prev]
}
let include_command = true;
let build_option_that_sets_include_to_false = {};
@@ -75,14 +81,18 @@ export function Step3SelectCommands(props){
return [...prev, {...cur, disabled: false, selected: false, reason: ""}];
}
}else{
return [...prev, {...cur, disabled: true, selected: false, reason: "Not available when build option \"" + build_option_that_sets_include_to_false["name"] + "\" is not \"" + build_option_that_sets_include_to_false["value"] + "\""}]
// command not possible due to build option, don't show it
// {...cur, disabled: true, selected: false, reason: "Not available when build option \"" + build_option_that_sets_include_to_false["name"] + "\" is not \"" + build_option_that_sets_include_to_false["value"] + "\""}
return [...prev]
}
}catch(error){
console.error(error);
}
}
}
return [...prev, {...cur, disabled: true, selected: false, reason: "This command isn't supported by the selected OS"}];
// command isn't supported by the OS, so don't even show it as an option
// , {...cur, disabled: true, selected: false, reason: "This command isn't supported by the selected OS"}
return [...prev];
}catch(error){
console.log(error);
return [...prev, {...cur, disabled: false, selected: false, reason: "Failed to parse command attributes"}];
@@ -100,35 +110,177 @@ export function Step3SelectCommands(props){
setCommandOptions(selectedCommands);
}
}
}
});
const finished = (selectedCommands) => {
const finished = () => {
let foundExit = false;
for(let i = 0; i < selectedCommands.length; i++){
if(selectedCommands[i]["supported_ui_features"].includes("callback_table:exit")){
for(let i = 0; i < selectedCommands.current.length; i++){
if(selectedCommands.current[i]["supported_ui_features"].includes("callback_table:exit")){
foundExit = true;
break;
}
}
if(foundExit){
const cmdNames = selectedCommands.map( c => c.cmd);
const cmdNames = selectedCommands.current.map( c => c.cmd);
props.finished(cmdNames);
}else if(props.buildOptions["agent_type"] === "agent") {
// only alert for agent types, not service types
confirmDialogCommands.current = selectedCommands.current;
setOpenConfirmDialog(true);
}else{
setOpenConfirmDialog(true);
setCommands(selectedCommands);
const cmdNames = selectedCommands.current.map( c => c.cmd);
props.finished(cmdNames);
}
}
const acceptConfirm = () => {
const cmdNames = commands.map( c => c.cmd);
const cmdNames = confirmDialogCommands.current.map( c => c.cmd);
props.finished(cmdNames);
}
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?"}
@@ -136,141 +288,137 @@ export function Step3SelectCommands(props){
acceptText="Accept"
onSubmit={acceptConfirm} />
}
{/* Navigation buttons - always at bottom */}
<div style={{flexShrink: 0}}>
<CreatePayloadNavigationButtons
first={props.first}
last={props.last}
canceled={canceled}
finished={finished}
/>
<br/><br/>
</div>
</div>
);
}
function CommandTransferSelect(props) {
const [checked, setChecked] = React.useState([]);
const [left, setLeft] = React.useState([]);
const [right, setRight] = React.useState([]);
const leftChecked = intersection(checked, left);
const rightChecked = intersection(checked, right);
const [hoveredCommand, setHoveredCommnad] = React.useState({});
function not(a, b) {
return a.filter( (value) => b.find( (element) => element["cmd"] === value["cmd"] && !element["disabled"] ) === undefined)
}
function intersection(a, b) {
return a.filter( (value) => b.find( (element) => element["cmd"] === value["cmd"] && !element["disabled"] ) !== undefined)
}
const [commands, setCommands] = React.useState([]);
const [leftFilter, setLeftFilter] = React.useState("");
const [rightFilter, setRightFilter] = React.useState("");
const handleToggle = (value) => () => {
let currentIndex = -1;
if(props.itemKey){
currentIndex = checked.findIndex( (element) => element[props.itemKey] === value[props.itemKey]);
}else{
currentIndex = checked.indexOf(value);
}
const newChecked = [...checked];
if (currentIndex === -1) {
newChecked.push(value);
} else {
newChecked.splice(currentIndex, 1);
}
setChecked(newChecked);
if(value.disabled){
return;
}
const newCommands = commands.map( c => {
if(c.cmd === value.cmd){
return {...c, selected: !c.selected};
}
return {...c};
});
setCommands(newCommands);
};
const handleAllRight = () => {
const initialLeft = props.commands.reduce( (prev, cur) => {
if(cur.disabled && !cur.selected){
return [...prev, {...cur}];
}else{
return [...prev];
}
}, [])
const initialRight = props.commands.reduce( (prev, cur) => {
if(!(cur.disabled && !cur.selected)){
return [...prev, {...cur}];
}else{
return [...prev];
}
}, [])
setLeft(initialLeft);
setRight(initialRight);
const newCommands = commands.map( c => {
if(c.disabled){return {...c}}
return {...c, left: false, right: true};
});
setCommands(newCommands);
};
const handleCheckedRight = () => {
setRight(right.concat(leftChecked));
setLeft(not(left, leftChecked));
setChecked(not(checked, leftChecked));
const newCommands = commands.map( c => {
if(c.selected && c.left){
return {...c, selected: false, left: false, right: true};
}
return {...c}
});
setCommands(newCommands);
};
const handleCheckedLeft = () => {
setLeft(left.concat(rightChecked));
setRight(not(right, rightChecked));
setChecked(not(checked, rightChecked));
const newCommands = commands.map( c => {
if(c.selected && c.right){
return {...c, selected: false, left: true, right: false};
}
return {...c}
});
setCommands(newCommands);
};
const handleAllLeft = () => {
const initialLeft = props.commands.reduce( (prev, cur) => {
if(!(cur.disabled && cur.selected)){
return [...prev, {...cur}];
}else{
return [...prev];
}
}, [])
const initialRight = props.commands.reduce( (prev, cur) => {
if((cur.disabled && cur.selected)){
return [...prev, {...cur}];
}else{
return [...prev];
}
}, [])
setLeft(initialLeft);
setRight(initialRight);
const newCommands = commands.map( c => {
if(c.disabled){return {...c}}
return {...c, left: true, right: false};
});
setCommands(newCommands);
};
useEffect( () => {
const initialLeft = props.commands.reduce( (prev, cur) => {
if(!cur.selected){
return [...prev, {...cur}];
}else{
return [...prev];
}
}, [])
const initialRight = props.commands.reduce( (prev, cur) => {
if(cur.selected){
return [...prev, {...cur}];
}else{
return [...prev];
}
}, [])
setLeft(initialLeft);
setRight(initialRight);
const newCommands = props.commands.map( c => {
if(c.selected){
return {...c, selected: false, left: false, right: true};
}
return {...c, selected: false, left: true, right: false};
});
setCommands(newCommands);
}, [props.commands]);
useEffect( () => {
props.updateSelectedCommands(commands.filter(c => c.right));
}, [commands]);
const setHoveredData = (event) => {
const cmd = props.commands.filter( c => c.cmd === event.target.innerText );
if(cmd.length > 0){
setHoveredCommnad(cmd[0]);
props.setHoveredCommand(cmd[0]);
}
}
const customList = (title, items) => (
<Paper style={{width:"100%", height: "calc(40vh)", display: "flex", flexDirection: "column", overflowY: "auto"}} elevation={5}>
<CardHeader title={title} />
<StyledDivider className={classes.divider}/>
<div style={{display: "flex", flexGrow: 1, overflowY: "auto", width: "100%"}}>
<List dense component="div" role="list" style={{padding:0, width: "100%", overflow: "auto"}}>
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>
);
@@ -278,110 +426,62 @@ function CommandTransferSelect(props) {
<ListItem />
</List>
</div>
</Paper>
);
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 item xs={5}>
{customList("Available Commands", left)}
</Grid>
<Grid item>
<Grid container direction="column" alignItems="center">
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleAllRight}
disabled={left.length === 0}
aria-label="move all right"
>
&gt;&gt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleCheckedRight}
disabled={leftChecked.length === 0}
aria-label="move selected right"
>
&gt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleCheckedLeft}
disabled={rightChecked.length === 0}
aria-label="move selected left"
>
&lt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleAllLeft}
disabled={right.length === 0}
aria-label="move all left"
>
&lt;&lt;
</StyledButton>
</Grid>
</Grid>
<Grid item xs={5}>
{customList("Commands Included", right)}
</Grid>
</Grid>
<Grid container justifyContent="center" alignItems="flex-start" className={classes.root}>
<Grid item xs={12} style={{height: "100%", marginBottom: "10px"}}>
{hoveredCommand["cmd"] !== undefined &&
<Paper className={classes.paper} style={{width: "100%"}} elevation={5}>
<CardHeader
title={
<React.Fragment>
{hoveredCommand["cmd"]}
<Button variant="contained" color="primary"
href={"/docs/agents/" + props.payload_type + "/commands/" + hoveredCommand["cmd"]}
style={{marginLeft: "10px", float: "right"}} target="_blank">Documentation
</Button>
</React.Fragment>
}
/>
<StyledDivider classes={{root: classes.divider}}/>
{hoveredCommand["reason"] !== "" ? (
<Typography variant="body1" align="left" component="div"
style={{"marginLeft": "10px"}}><b>{hoveredCommand["disabled"] ? ("Disabled Reason: ") : ("Information: ")} </b>{hoveredCommand["reason"]}
</Typography>
) : null}
<br/>
<Typography align="left" component="div"
style={{"marginLeft": "10px"}}><b>Commandline
Help: </b>{hoveredCommand["help_cmd"]}
</Typography>
<Typography align="left" component="div"
style={{"marginLeft": "10px"}}><b>Needs Admin
Permissions: </b>{hoveredCommand["needs_admin"] ? "True" : "False"}
</Typography>
<Typography align="left" component="div"
style={{"marginLeft": "10px"}}><b>Description: </b>{hoveredCommand["description"]}
</Typography>
</Paper>
}
</Grid>
</Grid>
</div>
<CreatePayloadNavigationButtons first={props.first} last={props.last} canceled={props.canceled}
finished={finished}/>
<br/><br/>
</div>
);
}
);
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={commands.filter(c => c.left && !c.disabled).length === 0}
aria-label="move all right"
>
&gt;&gt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleCheckedRight}
disabled={commands.filter(c => c.left && c.selected).length === 0}
aria-label="move selected right"
>
&gt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleCheckedLeft}
disabled={commands.filter( c => c.right && c.selected).length === 0}
aria-label="move selected left"
>
&lt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleAllLeft}
disabled={commands.filter(c => c.right && !c.disabled).length === 0}
aria-label="move all left"
>
&lt;&lt;
</StyledButton>
</div>
{customList("Commands in Payload", commands.filter(c => c.right), rightFilter, updateRightFilter)}
</div>
);
}
/*
<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>
*/
Loaded 100 of 512 files, more files were not shown because too many files have changed in this diff. Show more