Compare commits

..
684 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
github-actions ce5057e58d Bump Mythic Dockerfile tag to match release 'v3.3.0.27' 2024-10-28 18:47:21 +00:00
its-a-feature e8737b4223 updating missing hasura permissions 2024-10-28 13:32:15 -05:00
github-actions 767f82aa3d Bump Mythic Dockerfile tag to match release 'v3.3.0.26' 2024-10-28 17:16:00 +00:00
its-a-feature 5f85d9e716 fixing bug with processes not getting marked as deleted 2024-10-28 12:00:57 -05:00
github-actions 74536d4948 Bump Mythic Dockerfile tag to match release 'v3.3.0.25' 2024-10-28 14:44:20 +00:00
its-a-feature 5979d236bc ui fixes and proxy stop fix via MythicRPC
added support for setting cli history to be display params vs original params per agent
2024-10-28 09:13:56 -05:00
github-actions 641c8e1d60 Bump Mythic Dockerfile tag to match release 'v3.3.0.24' 2024-10-18 13:45:36 +00:00
its-a-feature 208611543c Updating to better reflected deleted payloads not making callbacks 2024-10-18 08:30:46 -05:00
github-actions dfad0dea3d Bump Mythic Dockerfile tag to match release 'v3.3.0.23' 2024-10-16 17:02:26 +00:00
its-a-feature f7cd34d9da UI update to save callbacks table column widths 2024-10-16 11:36:38 -05:00
github-actions db37b43465 Bump Mythic Dockerfile tag to match release 'v3.3.0.22' 2024-10-14 17:16:26 +00:00
its-a-feature 617d927472 Many UI Fixes and features added
added UDP Associate and more UI Tab-complete options
2024-10-14 11:25:33 -05:00
Cody Thomas 63a6d5e106 Merge pull request #411 from art-dsit/art/rootless_2
Prevent root-owned files ending up on the filesystem during CLI build
2024-10-08 08:20:48 -05:00
github-actions e58eead1fd Bump Mythic Dockerfile tag to match release 'v3.3.0.21' 2024-10-01 23:07:08 +00:00
Cody Thomas 52d33aef7a Merge pull request #409 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/multi-d66d039ac5
Bump serve-static and express in /MythicReactUI
2024-10-01 17:48:56 -05:00
Cody Thomas 2aa45ebb3a Merge pull request #385 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/ejs-3.1.10
Bump ejs from 3.1.9 to 3.1.10 in /MythicReactUI
2024-10-01 17:48:44 -05:00
Cody Thomas eb730689b0 Merge pull request #404 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/webpack-5.94.0
Bump webpack from 5.92.0 to 5.94.0 in /MythicReactUI
2024-10-01 17:48:34 -05:00
Cody Thomas 7369449c49 Merge pull request #410 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/multi-cf87d80143
Bump send and express in /MythicReactUI
2024-10-01 17:48:25 -05:00
Cody Thomas 22550d9022 Merge pull request #412 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/multi-9423f4c335
Bump body-parser and express in /MythicReactUI
2024-10-01 17:48:11 -05:00
Cody Thomas 150209dc8f Merge pull request #413 from its-a-feature/dependabot/npm_and_yarn/MythicReactUI/rollup-2.79.2
Bump rollup from 2.79.1 to 2.79.2 in /MythicReactUI
2024-10-01 17:47:29 -05:00
its-a-feature 4702b6a00c removing fade transitions to make a snappier UI 2024-10-01 17:47:00 -05:00
github-actions 122fe892af Bump Mythic Dockerfile tag to match release 'v3.3.0.20' 2024-09-29 16:14:40 +00:00
its-a-feature faaab0d3a1 Allow tab-cycle ChooseOne values on the command line in the UI 2024-09-29 10:59:37 -05:00
github-actions eaccded888 Bump Mythic Dockerfile tag to match release 'v3.3.0.19' 2024-09-27 20:22:29 +00:00
its-a-feature 273f9a5c71 UI QoL and proxy stop fixes 2024-09-27 15:07:30 -05:00
dependabot[bot] 3b58f30f3d Bump rollup from 2.79.1 to 2.79.2 in /MythicReactUI
Bumps [rollup](https://github.com/rollup/rollup) from 2.79.1 to 2.79.2.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.79.1...v2.79.2)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <[email protected]>
2024-05-02 13:58:50 +00:00
660 changed files with 79666 additions and 44019 deletions

No files matched your search

+15 -11
View File
@@ -9,7 +9,7 @@ on:
# types: [published]
push:
branches:
- 'docker_updates'
- 'Mythic3.3'
- 'master'
tags:
- "v*.*.*"
@@ -43,7 +43,7 @@ env:
# Set the container image version to the Github release tag
VERSION: ${{ github.ref_name }}
# Branch for pushing release changes (TODO: Change this to the main branch when the rewrite is finished)
# Branch for pushing release changes
RELEASE_BRANCH: master
jobs:
@@ -72,6 +72,8 @@ jobs:
# the following are unique to this job
- name: Lowercase the server container image name
run: echo "MYTHIC_SERVER_IMAGE_NAME=${MYTHIC_SERVER_IMAGE_NAME,,}" >> ${GITHUB_ENV}
- name: Copy version file over
run: cp VERSION mythic-docker/src/VERSION
- name: Build and push the server container image
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
with:
@@ -118,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
@@ -160,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
@@ -202,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
@@ -246,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
@@ -290,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
@@ -334,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
@@ -356,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:
@@ -378,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
@@ -436,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
@@ -548,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
+6
View File
@@ -25,6 +25,7 @@ venv.bak/
# ssl certs
ssl/
# Mythic files
nginx-docker/config/blockips.conf
mythic-docker/src/Mythic
mythic_access.*
mythic_sync/
@@ -164,3 +165,8 @@ _ReSharper*/
.vscode/
jupyter-docker/jupyter/MythicExamples/.ipynb_checkpoints/
nginx-docker/config/blockips.conf
/MythicReactUI/build/
/MythicReactUI/node_modules/
/.gocache/
+1238
View File
File diff suppressed because it is too large. Load diff
+46 -1
View File
@@ -130,4 +130,49 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.
MIT license
===========
Copyright (c) 2017 sql.js authors (see AUTHORS)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# Some portions of the Makefile taken from:
Copyright 2017 Ryusei Yamaguchi
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
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;
}
+6614 -5956
View File
File diff suppressed because it is too large. Load diff
+48 -41
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.8.6",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-brands-svg-icons": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@mui/icons-material": "^5.14.16",
"@mui/lab": "^5.0.0-alpha.138",
"@mui/material": "^5.14.16",
"@mui/x-charts": "^6.18.7",
"@mui/x-data-grid": "^6.18.6",
"@mui/x-date-pickers": "^6.17.0",
"ace-builds": "^1.31.1",
"anser": "^2.1.1",
"dayjs": "^1.11.10",
"elkjs": "^0.8.2",
"graphql": "^16.8.1",
"graphql-ws": "^5.14.2",
"@apollo/client": "^3.13.8",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@hello-pangea/dnd": "^17.0.0",
"@mui/icons-material": "^7.0.2",
"@mui/lab": "^7.0.0-beta.11",
"@mui/material": "^7.0.2",
"@mui/utils": "^7.0.2",
"@mui/x-charts": "^8.1.0",
"@mui/x-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.9.2",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
"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.2.0",
"react-ace": "^10.1.0",
"react": "^18.3.1",
"react-ace": "^14.0.1",
"react-colorful": "^5.6.1",
"react-dom": "^18.2.0",
"react-dom": "^18.3.1",
"react-draggable": "^4.4.6",
"react-moment": "^1.1.3",
"react-router-dom": "^6.18.0",
"react-router-dom": "^7.13.1",
"react-scrollbar-size": "^5.0.0",
"react-split": "^2.0.14",
"react-toastify": "^9.1.3",
"react-virtualized": "^9.22.5",
"react-virtualized-auto-sizer": "^1.0.20",
"react-window": "^1.8.9",
"reactflow": "^11.9.4",
"react-toastify": "^11.0.5",
"react-tooltip": "^5.28.1",
"react-virtualized": "^9.22.6",
"react-virtualized-auto-sizer": "^1.0.26",
"react-window": "^1.8.11",
"save-svg-as-png": "^1.4.17",
"semver": "^7.5.4",
"styled-components": "^6.1.0",
"tough-cookie": "^4.1.3",
"semver": "^7.6.2",
"sql.js": "^1.13.0",
"styled-components": "^6.1.17",
"uuid": "^9.0.1",
"webpack": "^5.89.0",
"webpack": "^5.105.0",
"word-wrap": "^1.2.5"
},
"scripts": {
"react-start": "CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=true WDS_POLLING=true BROWSER=none PORT=3000 WDS_SOCKET_PORT=7443 WDS_SOCKET_PATH=ws react-scripts start",
"react-build": "react-scripts build",
"react-test": "react-scripts test",
"react-start": "CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=true WDS_POLLING=true BROWSER=none PORT=3000 WDS_SOCKET_PORT=7443 WDS_SOCKET_PATH=ws react-app-rewired start",
"react-build": "react-app-rewired build",
"react-test": "react-app-rewired test",
"react-eject": "react-scripts eject",
"build": "npm run react-build",
"start": "cross-env BROWSER=none CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=true WDS_POLLING=true PORT=3000 WDS_SOCKET_PORT=7443 WDS_SOCKET_PATH=ws npm run react-start"
"start": "BROWSER=none CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=true WDS_POLLING=true PORT=3000 WDS_SOCKET_PORT=7443 WDS_SOCKET_PATH=ws npm run react-start"
},
"eslintConfig": {
"extends": [
+164
View File
@@ -0,0 +1,164 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.7.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Isolation_Mode" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 720 720" style="enable-background:new 0 0 720 720;" xml:space="preserve">
<style type="text/css">
.st0{fill:#E01F2F;}
</style>
<g>
<path class="st0" d="M631.71,544.4c-2.42-0.54-7.34-1.83-13.64-4.16c0.33,0.12,0.65,0.26,0.97,0.39
C624.37,542.61,628.45,543.68,631.71,544.4z"/>
<path class="st0" d="M633.4,544.75L633.4,544.75c-0.53-0.11-1.11-0.23-1.69-0.35C632.8,544.65,633.4,544.75,633.4,544.75z"/>
<path class="st0" d="M141.27,535.72C123.48,559.12,99,591.36,97.89,593.2c-1.18,1.97-7.99,13.54-20.39,18.46
c-17.32,5.6-21.78-2.86-21.78-2.86s-20.56,0.4-19.7,17.05h34.95v-0.02h33.31c0,0,0.58-14.58,15.16-17.49l60.92-48.82
C156.9,555.69,146.15,544.52,141.27,535.72z"/>
<g>
<path class="st0" d="M487.03,525.19l-76.29,72.09l72.21-76.16c-0.7-0.7-1.42-1.4-2.16-2.12c-9.15-8.9-17.68-13.07-26.83-13.07
c-10.91,0-22.71,5.71-37.09,13.4c2.02,14.66,2.11,33.84-7.43,46.78c-0.07,0.09-0.14,0.19-0.21,0.28
c-10.71,14.14-19.46,25.74-20.11,26.82c-1.19,1.98-8.11,13.75-20.72,18.61c-0.46,0.18-0.9,0.3-1.33,0.44
c-0.51,0.18-1.04,0.33-1.58,0.49c-0.7,0.19-1.4,0.37-2.07,0.49c-0.02,0-0.02,0-0.04,0c-11.13,2.07-16.38-4.44-16.38-4.44
s-20.56,0.4-19.7,17.05h34.95v-0.02h33.28c0,0,0.58-14.58,15.16-17.49c0,0,0,0,0.02-0.02c0.05-0.05,0.25-0.19,0.54-0.44l0.44-0.35
c6.99-5.78,49.59-41.09,73.3-66.94c3-3.27,5.69-6.39,7.96-9.27c-0.25-0.25-0.49-0.51-0.74-0.76
C490.59,528.89,488.89,527.1,487.03,525.19z"/>
<path class="st0" d="M384.77,207c0,0-0.02,20.99,7.18,27.11c7.2,6.15,22.41,1.46,32.15-9.76c11.84-13.64,18.61,5.09,18.61,5.09
c0.37-14.56-12.78-15.8-12.78-15.8c14.72-7.18,20.65,8.89,20.65,8.89c-0.67-7.2-4.21-15.58-16.17-15.56
c-16.51,0.04-18.79,17.6-33.3,20.07C386.6,229.51,384.77,207,384.77,207z"/>
<path class="st0" d="M681.85,338.39c-2.6-12.08-7.18-23.27-13-33.56c-5.83-10.29-13.01-19.69-21.41-28.04
c-8.38-8.34-18.05-15.77-29.27-21.3c-11.13-5.51-23.25-8.78-35.14-10.08c-11.92-1.3-23.62-0.74-34.79,0.84
c-11.15,1.6-21.88,4.23-31.96,7.08c-2.55,0.72-4.85,1.35-7.64,2.23c-1.35,0.42-2.67,0.79-4.04,1.26l-4.07,1.4
c-5.44,1.98-11.05,4.44-16.19,7.24c-1.32,0.68-2.53,1.39-3.76,2.09c-1.25,0.7-2.37,1.39-3.53,2.07l-6.46,3.88
c-4.11,2.46-8.08,4.64-12.19,6.71c-4.16,2.02-8.38,4-12.78,5.81l-3.28,1.37l-3.49,1.4l-6.92,2.74
c-9.15,3.56-18.16,6.76-27.22,9.32c-9.04,2.58-18.12,4.51-27.2,5.64c-9.08,1.12-18.14,1.4-27.13,0.74
c-1.12-0.09-2.25-0.14-3.37-0.23l-3.34-0.42l-3.35-0.42c-0.53-0.05-1.18-0.18-1.79-0.26l-1.81-0.28
c-4.78-0.77-9.4-1.62-13.96-2.63c-9.11-2.02-17.91-4.69-25.94-8.2c-8.03-3.51-15.24-7.92-20.67-13.21
c-5.46-5.3-8.94-11.31-10.55-18.25c-1.6-7.01-1.3-15.05,0.76-22.79c1-3.88,2.42-7.69,4.23-11.31c1.79-3.64,3.93-7.09,6.43-10.24
c2.48-3.18,5.25-6.08,8.31-8.57c3.04-2.51,6.32-4.69,9.82-6.39c1.72-0.91,3.58-1.51,5.36-2.28c1.86-0.53,3.69-1.23,5.62-1.56
c3.81-0.86,7.76-1.32,11.82-1.26c0.02,0,0.04,0,0.05,0c2.83,0.02,5.62,0.49,8.34,1.21c1.46,0.39,2.97,0.74,4.5,1.04
c3.93,0.77,7.39,1.58,10.47,2.44c0.23,0.09,0.46,0.18,0.68,0.26l0.04-0.07c3,0.84,5.62,1.72,7.87,2.58v-0.11
c0.86,0.37,1.69,0.76,2.46,1.12c0.02,0,0.02,0,0.04,0.02c10.78,5.22,12.05,9.69,12.05,9.69c0.04,0.09,0.09,0.12,0.12,0.21
c2.05,2.79,3.48,5.57,4.95,7.88c5.87,8.99,8.34,11.03,8.34,11.03l1.86-3.83c-7.55-8.34-9.78-32.65-9.78-32.65
s15.95-3.3,23.13,0.12c6.95,3.35,0.95,16.12,0.6,16.74c5.65-2.35,11.43-13.79,11.43-13.79c4.74-2.69,3.53-11.17,3.53-11.17
c-18.95-22.83-44.53-26.03-65.87-22.14c-2.48,0.04-4.92,0.14-7.34,0.33c-4.5,0.33-9.01,0.4-13.5,0.11
c-1.18-0.07-2.35-0.12-3.53-0.16c-6.09-0.18-12.29,0.39-18.37,1.65c-3.07,0.54-6.04,1.51-9.04,2.39
c-2.92,1.11-5.88,2.12-8.68,3.51c-5.65,2.65-10.94,6.02-15.77,9.87c-4.85,3.85-9.2,8.24-13.03,13c-3.86,4.74-7.18,9.89-9.96,15.3
c-2.79,5.41-5.04,11.1-6.67,17c-3.28,11.78-4.27,24.53-1.62,37.37c1.39,6.41,3.69,12.78,6.92,18.56
c3.21,5.8,7.27,11.01,11.73,15.51c4.48,4.51,9.34,8.34,14.36,11.63c5.02,3.28,10.2,6.06,15.44,8.45
c10.47,4.78,21.13,8.1,31.77,10.62c5.3,1.26,10.64,2.32,15.86,3.23l1.93,0.33c0.65,0.11,1.23,0.23,1.97,0.32l4.18,0.6l4.2,0.58
c1.4,0.14,2.81,0.25,4.21,0.37c11.22,0.97,22.5,0.76,33.56-0.44c22.14-2.37,43.22-8.82,62.78-16.17l7.25-2.76l3.58-1.39l3.74-1.49
c4.99-1.97,9.99-4.21,14.96-6.55c4.95-2.41,9.9-5.06,14.54-7.74c4.65-2.69,8.78-5.22,12.57-7.16c3.79-1.98,7.52-3.55,11.61-4.99
l3.13-1.02c1.04-0.35,2.16-0.63,3.23-0.95c2.05-0.61,4.65-1.3,6.97-1.93c9.45-2.55,18.49-4.58,27.41-5.74
c8.9-1.12,17.61-1.39,25.83-0.39c8.22,1,15.93,3.25,22.93,6.81c6.94,3.51,13.54,8.6,19.44,14.61
c5.88,6.01,11.06,12.92,15.12,20.32c4.07,7.38,7.09,15.21,8.68,22.99c1.55,7.76,1.6,15.38,0.02,22.23
c-1.6,6.85-4.71,12.98-9.52,18.32c-4.74,5.36-11.08,9.92-18.28,13.4c-7.2,3.48-15.26,5.9-23.39,7.11
c-4.06,0.61-8.15,0.91-12.12,0.88c-1,0.02-1.97-0.05-2.95-0.09l-2.81-0.28c-1.91-0.07-3.65-0.51-5.44-0.81
c-0.91-0.11-1.74-0.4-2.6-0.65c-0.86-0.26-1.72-0.44-2.58-0.74c-1.69-0.68-3.39-1.18-5.11-2.05l-1.3-0.58l-0.65-0.3l-0.67-0.37
l-2.67-1.44c-0.9-0.49-1.86-1.11-2.79-1.63l-1.4-0.84l-1.49-0.95c-3.86-2.42-8.29-5.37-13.01-8.25l-3.65-2.18
c-1.21-0.72-2.56-1.42-3.85-2.12c-2.55-1.44-5.36-2.72-8.1-4.02c-0.68-0.33-1.39-0.63-2.11-0.91l-2.14-0.86
c-1.44-0.56-2.86-1.18-4.32-1.67l-4.43-1.42c-1.48-0.47-2.95-0.95-4.46-1.26l-4.48-1.07c-0.76-0.18-1.49-0.35-2.25-0.51
l-2.27-0.39c-3-0.56-6.04-0.9-9.06-1.25c-6.08-0.54-12.15-0.67-18.18-0.37c-1.49,0.09-2.99,0.25-4.48,0.35l-2.25,0.19l-2.21,0.3
l-4.43,0.6l-4.36,0.81c-11.61,2.21-22.62,5.88-32.96,10.41c-5.18,2.25-10.19,4.76-15.08,7.39c-2,1.12-4.02,2.23-6.01,3.39
c-36.46,10.5-75.81,5.09-107.56-16.07c-0.84-0.56-1.72-1.16-2.6-1.76c-1.09-0.09-2.04-0.25-3.14-0.35
c-3.74-0.49-7.38-0.65-11.06-0.65c-4.23,0-8.38,0.4-12.42,1.09l-2.9,0.51l-2.99,0.74c-0.95,0.19-1.84,0.49-2.84,0.76
c-0.95,0.35-1.93,0.54-2.79,0.95c-3.85,1.3-7.48,3.09-11.12,4.99c2.9-2.95,6.18-5.55,9.87-7.57c3.74-2,7.64-3.76,11.71-4.85
c4.11-1.14,8.39-1.84,12.57-2.09c2.9-0.16,5.69-0.16,8.62,0c-6.22-7.73-19.79-23.18-31.14-27.08c0,0-39.74-30.17-52.4-39.72
c-12.71-9.68-27.17-27.13-24.94-32.86c0,0,9.57-13.96,17.89-16.54c8.27-2.7,10.47-12.28,10.47-12.28s-12.28,3.49-27.57,3.93
s-22.23,4.39-22.23,4.39s-8.78-7.04-18.81-8.34c0,0,0.84-30.15,40.64-49.4c39.78-19.18,86.49-44.5,104.42-98.2
c-1.49,2.35-24.97,39.6-53.28,50.1c-29.26,10.92-66.43,23.18-78.2,30.64c-11.85,7.43-34.56,32.35-47.7,79.13l-49.31,57.72
c0,0,2.58,6.97,6.94,6.53c0,0,1.3,9.57,10.01,5.62c0,0,2.23,10.08,0.46,14.01c-1.76,3.95,4.43,16.16,12.66,18.35l-0.79-4.79
c0,0,2.16,5.23,9.57,7.43c0,0-7.83-10.06-4.85-19.69c2.56-7.92,1.53-11.77,1.05-12.91l3.79,7.67c0,0,1.28-10.47-4.04-18.68
c1.93,2.49,11.36,12.92,31.26,5.94c1.79-1.62,3.49-3.28,5.04-4.99l2.18-2.53c0.76-0.81,1.32-1.76,2-2.65
c1.39-1.7,2.49-3.58,3.83-5.43c0.6-0.95,1.16-1.93,1.7-2.95c0.6-0.95,1.19-1.93,1.9-2.88c1.11-2,2.25-3.99,3.6-6.04
c-0.16,2.39-0.49,4.74-0.9,7.08c-0.39,1.19-0.65,2.35-0.98,3.49l-0.65,1.69c-0.21,0.61-0.3,1.16-0.6,1.7
c-0.49,1.09-0.98,2.23-1.55,3.28c-0.49,1.14-1.09,2.12-1.84,3.2c-0.6,0.98-1.23,2.09-1.93,3.04l-2.35,2.83
c-1.69,1.84-3.58,3.6-5.57,5.15c-0.46,4.23-1.7,10.22-4.39,15.73c-0.39,0.91-0.79,1.76-1.14,2.55c1.19-1,24.15-19.55,60.08-3.06
l1.9,0.79l-0.98,1.84l-2.65,4.58c4.79-1.14,9.68-1.19,14.47-0.11c6.41,1.55,11.77,6.09,15.3,11.27c3.69,5.2,5.74,11.22,7.48,17
c0.84,2.9,1.79,5.83,3.42,8.24c1.65,2.44,4.39,4.04,7.48,4.5c-3.09-0.05-6.23-1.35-8.18-3.9c-2.05-2.49-3.2-5.43-4.34-8.24
c-2.2-5.72-4.39-11.41-8.1-16.03c-3.58-4.6-8.38-8.24-13.91-9.54c-5.53-1.3-11.66-0.49-16.74,1.7l-4.95,2.09l2.55-4.74l3.79-6.83
c-7.43-3.04-15.4-4.64-23.32-3.9c-8.62,0.76-16.95,3.99-24.43,8.57c-0.05,0.05-0.11,0.09-0.16,0.16
c-9.71,5.83-17.4,14.45-22.93,24.32c-6.08,10.82-13.91,25.13-19.14,35.6c-9.68,19.19-18.35,22.74-18,31.45
c0.46,8.73-4.34,38.88-7.78,45.03c0,0,0.79,9.68,10.87,8.71c0,0,0,7.48,10.01,6.57c0,0-6.13-9.17,2.69-13.56
c8.78-4.34,14.4-0.39,16.56,4.79c2.2,5.27-4.79,12.85-10.22,8.82l-2.44-3.99c0,0-6.97,11.36,6.53,14.42c0,0,1.35,7.92,15.35,7.48
c0,0-7.87-3.93-1.69-10.08c0,0,10.41-3.93,8.32-23.62c0,0,0.25-1.4,1.19-2.25c0.14-1.88,0.32-3.78,0.65-5.62
c0.6-3.99,1.33-7.88,3.23-11.82c0.86-1.93,2.05-3.99,4.25-5.57l0.65-0.46l0.54-0.35c0.3-0.25,0.65-0.46,1.04-0.79
c0.56-0.49,1.35-1.05,1.84-1.69c2.49-2.55,4.69-5.58,6.48-8.87c1.98-3.2,3.69-6.69,5.18-10.19c-0.3,3.9-1.28,7.64-2.6,11.38
c-1.28,3.74-3.04,7.38-5.62,10.66c-0.65,0.84-1.35,1.63-2.11,2.39c-0.39,0.39-0.79,0.68-1.23,1.11l-0.6,0.49l-0.35,0.25
l-0.19,0.16c-0.81,0.74-1.62,2.12-2.25,3.74c-1.25,3.14-2.05,6.78-2.55,10.41c-0.35,2.2-0.6,4.34-0.79,6.48
c0.76,0.7,1.49,1.6,2.35,2.74c7.13,9.78,0.79,25.18,23.72,36.46c-0.84-2.09-1.69-4.28-2.42-6.48c-2.3-7.08-3.85-14.56-4.23-22.02
c-0.26-3.79-0.21-7.57,0-11.38c0.19-3.74,0.79-7.48,1.39-11.22c0.44-1.84,0.74-3.74,1.19-5.53l0.68-2.79l0.81-2.69
c0.54-1.79,1.19-3.6,1.88-5.37l0.79-2.55c0.37-0.79,0.56-1.6,0.81-2.49c1.93-6.73,2.83-13.77,1.9-20.63
c-0.9-6.78-3.49-13.4-7.78-18.79c-0.98-1.35-2.14-2.65-3.42-3.79c-1.25-1.14-2.65-2.16-4.09-3.09c-1.46-0.9-2.99-1.6-4.64-2.14
c-1.55-0.51-3.2-0.79-4.9-0.84c-3.55-0.16-6.94,0.49-10.03,2.35c-1.49,0.9-2.9,2.04-4.04,3.39c-1.25,1.3-2.35,2.84-3.06,4.48
c0.91-3.53,3.06-6.88,6.2-9.27c2.93-2.42,6.92-3.74,10.85-3.93c1.95-0.09,3.93,0,5.9,0.46c1.93,0.46,3.88,1.11,5.62,2.09
c1.76,0.84,3.44,2,4.93,3.2c1.55,1.23,2.99,2.6,4.28,4.04c2.65,2.95,4.83,6.29,6.34,9.87c1.69,3.58,2.9,7.38,3.34,11.26
c1.14,7.78,0.39,15.66-1.95,23.04c-0.25,0.9-0.54,1.83-0.84,2.74l-0.98,2.65c-0.61,1.7-1.25,3.39-1.76,5.09
c-1.16,3.39-2.09,6.88-2.9,10.38c-0.65,3.49-1.39,7.02-1.74,10.62c-0.3,3.53-0.6,7.18-0.49,10.76c0,3.6,0.19,7.18,0.54,10.76
c0.39,3.6,1.09,7.13,1.83,10.71c0.65,2.79,1.51,5.57,2.35,8.32c0,0.05,0.05,0.05,0.05,0.05s0,0.05,0.05,0.16
c1.33,4.13,3.09,8.11,5.13,11.98c2.79,3.88,6.94,8.08,13.26,11.56c6.34,3.42,14.82,6.27,26.18,7.32
c41.97,3.93,41.06,13.12,46.77,20.55c0,0,3.93-15.26-4.79-22.74c0,0,6.08-0.46,21.39,14.86c15.3,15.31,25.41,7.88,25.41,7.88
s-11.45,1.3-16.6-7.88c-5.27-9.17-7.52-30.56-14.51-34.93v-1.79c-1.09-1.25-2.2-2.55-3.35-3.9c-2.55-3.14-4.83-6.43-7.13-9.87
c-2.12-3.39-4.23-6.92-5.83-10.85c3.49,2.39,6.53,5.2,9.48,8.01c2.35,2.41,4.53,4.85,6.73,7.38c2.2,2.6,4.28,5.23,6.23,8.04
c3.14,4.53,5.9,9.11,8.13,14.36h0.05c13.72,17.44,32.15,43.62,28.57,55.07c-1.55,4.74-3.79,7.64-6.18,9.38
c-6.78,4.88-15.26,0.65-15.26,0.65s-20.58,0.44-19.69,17.05h62.5c0,0,7.43-10.52,16.26-13.96c8.62-3.51-19.37-72.54-19.74-73.48
c0.67,0.39,26.39,15.45,73.86-6.53c48.12-22.28,72.19-48.89,104.89-17.03c32.75,31.91,21.83,34.09,121.54,53.79
c0,0,34.1,13.12,19.65,38.88c0,0,0.11,0.44,0,1.09v-1.09c-10.22,9.15-16.4,1.3-16.4,1.3s-20.55,0.44-19.63,17.05h67.59
c0,0-1.84-10.52,7.32-13.96c9.24-3.53-10.06-27.97-16.16-50.29c-4.58-16.89-4.53-14.75-15.35-16.86c0,0-0.02,0-0.04-0.02
c-0.02,0-0.04-0.02-0.07-0.04c-0.05,0-0.16,0-0.25-0.05h-0.25c-0.25-0.05-0.6-0.16-1.09-0.25h-0.05c-0.21-0.05-0.4-0.09-0.61-0.16
c-0.11-0.02-0.19-0.05-0.3-0.07s-0.23-0.05-0.35-0.07c-0.05,0-0.11,0-0.19-0.05c-0.11,0-0.21-0.05-0.25-0.05
c-0.05-0.04-0.09-0.04-0.16-0.05c-0.02,0-0.04-0.02-0.05-0.02s-0.04-0.02-0.04-0.02c-0.04-0.02-0.04-0.02-0.07-0.04
c-0.05,0-0.09-0.05-0.19-0.05h-0.11c-0.09-0.05-0.19-0.05-0.3-0.05c-0.7-0.21-1.55-0.4-2.41-0.65c-0.09,0-0.16-0.05-0.25-0.05
c-0.11,0-0.16-0.05-0.25-0.09c-0.26-0.09-0.53-0.18-0.81-0.26c-0.19-0.05-0.39-0.11-0.56-0.18c-0.18-0.07-0.37-0.11-0.54-0.18
c-0.18-0.05-0.37-0.11-0.54-0.18c-0.3-0.11-0.6-0.19-0.91-0.26c-1.14-0.4-2.46-0.79-3.74-1.3c-0.16-0.11-0.33-0.16-0.49-0.25
c0-0.02-0.02-0.02-0.02-0.02h-0.02c-0.11-0.04-0.18-0.05-0.23-0.09c-0.04-0.02-0.07-0.04-0.11-0.05c0,0,0,0-0.02-0.02
s-0.05-0.04-0.09-0.05c-8.9-3.34-20.58-8.85-31.89-17.65c0,0-0.04-0.04-0.11-0.09c-3.9-3.04-7.76-6.44-11.43-10.29
c-0.04-0.04-0.07-0.07-0.11-0.11c-13.26-12.8-34.72-36.3-47.31-64.59c-0.35-0.79-0.74-1.58-1.12-2.35c0-0.02-0.02-0.02-0.02-0.02
c-3.25-6.37-7.92-12.15-13.49-17.37c-0.3-0.28-0.6-0.56-0.9-0.83c0.02,0,0.04,0.02,0.05,0.02c-4.79-4.69-10.1-8.9-15.61-12.66
l2.81,0.84c0.93,0.3,1.84,0.7,2.77,1.05l1.39,0.53c0.46,0.16,0.93,0.35,1.39,0.58c1.83,0.88,3.67,1.63,5.53,2.69
c0.93,0.51,1.84,0.95,2.81,1.51l2.88,1.69c0.95,0.53,2,1.21,2.99,1.83l3.09,1.91l3.21,2.02l3.44,2.12
c9.15,5.71,21.35,11.94,34.28,14.31l4.81,0.83c1.62,0.32,3.18,0.35,4.78,0.51l4.76,0.37c1.56,0.04,3.13,0.11,4.65,0.07
c6.16-0.02,12.2-0.56,18.16-1.53c11.87-1.93,23.43-5.51,34.4-10.98c5.5-2.72,10.82-5.97,15.89-9.75
c5.08-3.76,9.85-8.15,14.14-13.1c4.3-4.93,8.08-10.5,11.08-16.47c3-5.99,5.25-12.35,6.64-18.83
C684.92,363.52,684.41,350.44,681.85,338.39z M379.22,175.86l15,10.55l-23.46-8.52L379.22,175.86z M85.99,279.4l11.61-13.42h8.43
L85.99,279.4z M150.4,270.47l5.6-12.15l29.31-10.52l-23.99,11.36L150.4,270.47z M70.99,449.54l11.61-13.36h8.52L70.99,449.54z
M170.54,512c-3.69-5.58-5.64-12.36-6.04-19.11c-0.25-3.34-0.05-6.73,0.35-10.06c0.25-0.86,0.35-1.7,0.54-2.6
c0.19-1.05,0.4-1.6,0.56-2.44c0.44-1.49,0.68-2.99,0.98-4.53c1.14-6.18,1.69-12.52,1.79-18.81c0.32-12.61-1.39-25.32-4.64-37.48
c-0.84-3.09-1.84-6.04-2.99-8.92c-1.14-2.95-2.6-5.74-4.23-8.32c-1.7-2.65-3.69-5.08-6.13-7.02c-1.25-0.95-2.53-1.81-3.99-2.49
c-1.44-0.65-2.95-1.16-4.53-1.25c3.2-0.46,6.59,0.65,9.48,2.35c1.74,0.98,3.39,2.23,4.79,3.6c0.65-1.46,1.19-2.9,1.44-4.41
c0.35-1.53,0.44-3.09,0.44-4.64c-0.09-3.04-0.79-6.04-1.93-8.87c-1.09-2.84-2.6-5.58-4.34-8.18c-1.4-2.18-2.95-4.34-4.65-6.27
l-2.48,0.81c-1.25,0.46-2.55,0.95-3.83,1.39c-1.26,0.54-2.46,1.09-3.69,1.65l-1.7,0.98c-0.6,0.3-1.19,0.6-1.79,1
c-2.25,1.39-4.28,3.04-5.94,5.09c-0.51,0.54-0.84,1.09-1.16,1.63c-0.25,0.3-0.46,0.6-0.6,0.9c-0.09,0.3-0.35,0.6-0.44,0.91
c-0.54,1.23-0.9,2.53-1.05,3.99c-0.3-1.4-0.3-2.9-0.11-4.36c0.11-0.68,0.35-1.44,0.54-2.12c0.35-0.65,0.6-1.46,0.95-2.05
c0.74-1.39,1.55-2.53,2.6-3.74l1.44-1.63c0.61-0.49,1.16-0.95,1.76-1.46c0.54-0.54,1.19-0.93,1.74-1.33l1.84-1.3
c1.25-0.65,2.55-1.46,3.83-2.05c1.39-0.6,2.69-1.25,4.04-1.76l4.09-1.44l1.35-0.46l1,1.04c1.28,1.35,2.25,2.65,3.28,3.99
c1.05,1.35,2,2.74,2.79,4.2c1.79,2.88,3.2,6.02,4.13,9.41c0.4,1.6,0.79,3.28,0.9,5.04c0.16,1.74,0.05,3.49-0.16,5.18
c-0.35,1.69-0.79,3.34-1.49,4.88c-0.81,1.46-1.7,2.84-2.76,4c0.98,0.95,1.84,1.9,2.76,2.97c2.12,2.6,3.88,5.44,5.43,8.43
c1.55,2.95,2.84,6.09,3.88,9.17c2.2,6.29,3.69,12.71,4.76,19.25c0.95,6.53,1.33,13.15,1.14,19.79
c-0.16,6.62-0.98,13.29-2.46,19.79c-0.35,1.6-0.9,3.2-1.39,4.83c-0.19,0.76-0.54,1.69-0.68,2.2c-0.19,0.68-0.35,1.49-0.46,2.2
c-0.65,2.99-1,6.11-1,9.22c0,3.09,0.35,6.18,1.05,9.18c0.65,3.09,1.63,5.99,3.09,8.82c1.44,2.74,3.28,5.34,5.43,7.67
c2.11,2.34,4.53,4.5,7.18,6.32C179.46,522.03,174.23,517.67,170.54,512z M207.58,458.21c0.25,2.49,0.76,4.97,1.35,7.57
c0.65,2.48,1.6,4.93,2.53,7.48c-2.04-1.79-3.79-4.04-5.18-6.43c-1.35-2.44-2.41-5.02-3.2-7.81c-0.68-2.69-1.04-5.5-1.23-8.29
c-0.16-2.83,0-5.62,0.35-8.36c0.6-5.6,2.2-10.98,4.34-16c2.2-4.99,4.83-9.78,8.53-13.91c-1.49,5.18-3.04,10.26-4.25,15.4
c-1.23,5.04-2.18,10.12-2.9,15.21C207.32,448.1,207.18,453.17,207.58,458.21z M601.46,548.79c5.23,0.49,10.36,1.46,15.44,2.9
c2.49,0.76,4.99,1.65,7.34,2.79c2.39,1.19,4.69,2.6,6.73,4.43l1.46,1.55c0.39,0.49,0.74,0.98,1.19,1.49
c0.79,1.05,1.65,2.11,2.46,3.2c1.49,2.09,3.04,4.23,4.37,6.5c2.65,4.48,5.08,9.11,7.18,14l-0.19,0.11
c-3.2-4.2-6.09-8.53-8.97-12.82l-4.36-6.48c-0.65-1.09-1.39-2.14-2.12-3.25c-0.4-0.49-0.81-1.04-1.11-1.6l-1.14-1.35
c-1.84-1.74-3.9-3.23-6.13-4.37c-2.3-1.25-4.64-2.23-7.13-3.09c-4.83-1.74-9.87-2.93-15.07-3.74L601.46,548.79z M438.98,462.85
c-1.6,0.95-3.04,2.09-4.39,3.28c-1.35,1.14-2.6,2.55-3.69,3.99c-0.6,0.65-1.05,1.39-1.55,2.16c-0.46,0.76-0.9,1.55-1.19,2.23v0.05
l-0.16,0.21c-2.55,4.07-5.34,8.08-8.59,11.68c-1.39,1.93-3.18,3.64-4.88,5.43c-0.84,0.84-1.69,1.6-2.6,2.49
c-0.95,0.79-1.74,1.63-2.74,2.39c-7.39,6.34-15.72,11.52-24.53,15.51c-8.78,4.04-18.16,6.78-27.57,8.52
c-4.79,0.84-9.48,1.46-14.29,1.65c-2.46,0.16-4.85,0.19-7.24,0.16c-2.35-0.05-4.69-0.16-7.04-0.46
c9.41-1.05,18.65-2.84,27.61-5.34c4.55-1.19,8.92-2.69,13.36-4.28c4.28-1.6,8.62-3.34,12.77-5.39
c8.32-3.99,16.24-8.82,23.37-14.59c7.24-5.72,13.66-12.42,19.44-19.74c1.05-1.79,2.25-3.2,3.64-4.55
c1.35-1.44,2.95-2.69,4.58-3.74c1.65-1.09,3.34-1.98,5.15-2.63c1.79-0.76,3.74-1.16,5.57-1.51c-0.84,0.4-1.79,0.7-2.58,1.19
C440.67,461.95,439.77,462.35,438.98,462.85z M507.47,503.47l2.49,3.6c0.84,1.11,1.6,2.28,2.49,3.39l2.65,3.34l1.39,1.65
l1.39,1.49c1.02,1.05,2,2.05,2.99,3.09l3.09,2.84c1.09,1.05,2.2,1.9,3.28,2.79c1.09,0.9,2.2,1.79,3.39,2.63
c4.53,3.39,9.43,6.39,14.51,8.94c5.18,2.55,10.57,4.74,16.16,6.18l-0.16,0.54c-5.8-0.98-11.38-2.79-16.7-5.09
c-5.34-2.23-10.57-4.97-15.44-8.22c-1.32-0.68-2.49-1.63-3.71-2.49c-1.19-0.9-2.34-1.69-3.48-2.67l-3.35-2.84
c-1.09-0.93-2.2-1.98-3.28-2.99l-1.55-1.53l-1.49-1.65l-3.04-3.28c-0.95-1.14-1.81-2.28-2.69-3.49l-2.69-3.42l-1.3-1.65
l-1.39-1.55c-0.86-1.09-1.84-1.98-2.79-2.99c-2.05-1.83-4.18-3.64-6.43-5.18c-2.3-1.62-4.58-2.95-7.08-4.23
c-2.39-1.25-4.88-2.46-7.52-3.39c-10.27-4.04-21.58-5.9-33.12-6.2l-0.09-0.6c5.69-1.55,11.63-2.05,17.54-1.81
c2.93,0.11,5.99,0.46,8.87,0.91c2.99,0.49,5.94,1.3,8.82,2.2c1.4,0.46,2.79,0.98,4.2,1.55c1.39,0.6,2.79,1.23,4.14,1.98
c1.33,0.65,2.58,1.46,3.93,2.3c0.6,0.39,1.25,0.84,1.9,1.23c0.61,0.51,1.21,0.95,1.81,1.4C500.04,494,504.13,498.55,507.47,503.47
z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

+196 -8
View File
@@ -1,42 +1,230 @@
import { makeVar } from '@apollo/client';
import React from 'react';
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");
localStorage.removeItem("user");
// retrieve all cookies
let Cookies = document.cookie.split(';');
// set past expiry to all cookies
for (let i = 0; i < Cookies.length; i++) {
document.cookie = Cookies[i] + "=; expires="+ new Date(0).toUTCString();
}
meState({
loggedIn: false,
access_token: null,
refresh_token: null,
user: null
});
mePreferences(operatorSettingDefaults);
snackActions.clearAll();
restartWebsockets();
if(restart_websockets){
restartWebsockets();
}
}
+380 -118
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';
@@ -33,101 +34,262 @@ import { RefreshTokenDialog } from './RefreshTokenDialog';
import { MythicDialog } from './MythicComponents/MythicDialog';
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({
palette: {
primary: {
main: themeMode === "dark" ? "#465a79" : "rgb(102,121,145)",
() => {
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' ? '#ffffff' : '#30455e',
default: themeMode === "dark" ? 'rgb(31, 31, 31)' : '#ffffff',
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',
textBackgroundColorMythic: 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',
},
pageHeaderColor: "white",
folderColor: '#f1d592',
tableHeader: '#484848',
successOnMain: '#1ae302',
errorOnMain: '#ff656b',
infoOnMain: '#67ceff',
materialReactTableHeader: themeMode === 'dark' ? '#484848' : '#d5d5d5',
tableBorder: themeMode === 'dark' ? 'rgba(81,81,81,1)' : 'rgba(224,224,224,1)',
tableHover: themeMode === 'dark' ? 'rgba(85,88,93)' : 'rgba(245, 245, 245)',
pageHeader: {
main: '#827E80',
},
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{
@@ -135,57 +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: '56px', 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 5px 5px 5px', flexGrow: 1, flexDirection: 'column', height: "calc(100% - 4rem)", }}>
<Routes>
<Route path='/new/login' element={<LoginForm 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>} />
</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,26 +1,12 @@
import React, {useEffect} from 'react';
import {gql, useSubscription} from '@apollo/client';
import {snackActions} from './utilities/Snackbar';
import {getSkewedNow} from "./utilities/Time";
//fromNow must be in ISO format for hasura/postgres stuff
//new Date().toISOString() will do it
/*
const subscribe_payloads = gql`
subscription EventFeedNotificationSubscription($fromNow: timestamp!, $operation_id: Int!) {
operationeventlog(limit: 1, where: {deleted: {_eq: false}, timestamp: {_gte: $fromNow}, operation_id: {_eq: $operation_id}}, order_by: {id: desc}) {
operator {
username
}
id
message
level
resolved
source
}
}
`;
*/
const subscribe_payloads = gql`
subscription EventFeedNotificationSubscription($fromNow: timestamp!) {
operationeventlog_stream(cursor: {initial_value: {timestamp: $fromNow}, ordering: ASC}, batch_size: 1, where: {deleted: {_eq: false}}) {
operator {
@@ -31,54 +17,53 @@ subscription EventFeedNotificationSubscription($fromNow: timestamp!, $operation_
level
resolved
source
warning
}
}
`;
export function EventFeedNotifications(props) {
const me = props.me;
const fromNow = React.useRef( (new Date()).toISOString() );
const { loading, error, data } = useSubscription(subscribe_payloads, {
variables: {fromNow: fromNow.current},
const [fromNow, setFromNow] = React.useState(getSkewedNow().toISOString());
//const fromNow = React.useRef( );
useSubscription(subscribe_payloads, {
variables: {fromNow: fromNow },
fetchPolicy: "no-cache",
shouldResubscribe: true,
onError: (errorData) => {
snackActions.warning("Failed to get event notifications");
console.log(errorData);
},
onData: ({data}) => {
if (data.data.operationeventlog_stream.length > 0){
if(data.data.operationeventlog_stream[0].level === "api" ||
data.data.operationeventlog_stream[0].level === "debug" ||
data.data.operationeventlog_stream[0].level === "agent"
){
return;
}
if(data.data.operationeventlog_stream[0].resolved){
return;
}
if(data.data.operationeventlog_stream[0].operator){
const message = data.data.operationeventlog_stream[0].operator.username + ":\n" + data.data.operationeventlog_stream[0].message;
if (data.data.operationeventlog_stream[0].warning) {
snackActions.warning(message, { autoClose: 2000});
} else {
snackActions.info(message, { autoClose: 2000});
}
}else {
if (data.data.operationeventlog_stream[0].warning) {
snackActions.warning(data.data.operationeventlog_stream[0].message, {autoClose: 3000});
} else {
snackActions.info(
<div style={{width: "100%"}}>{data.data.operationeventlog_stream[0].message}</div>,
{autoClose: 3000});
}
}
}
}
});
useEffect( () => {
//console.log(data, loading, error, fromNow.current);
if(!loading && !error && data && data.operationeventlog_stream.length > 0){
if(data.operationeventlog_stream[0].source === "debug" || data.operationeventlog_stream[0].level === "debug" ){
return;
}
if(data.operationeventlog_stream[0].resolved){
return;
}
if(data.operationeventlog_stream[0].operator){
const message = data.operationeventlog_stream[0].operator.username + ":\n" + data.operationeventlog_stream[0].message;
if (data.operationeventlog_stream[0].level === "warning") {
snackActions.warning(message, { autoClose: 2000});
} else {
snackActions.info(message, { autoClose: 2000});
}
}else {
if (data.operationeventlog_stream[0].level === "warning") {
snackActions.warning(data.operationeventlog_stream[0].message, {autoClose: 3000});
} else {
snackActions.info(
<div style={{width: "100%"}}>{data.operationeventlog_stream[0].message}</div>,
{autoClose: 3000});
}
}
}else if(error){
console.error(error);
snackActions.error("Mythic encountered an error getting operational event stream", {autoHideDuration: 2000});
}
}, [loading, data, error, me.user]);
return null;
return null
}
@@ -0,0 +1,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, is_p2p}) => {
const theme = useTheme();
if(payload_type === ""){
return;
}
return (
<MythicStyledTooltip title={payload_type} >
<img src={"/static/" + payload_type + "_" + theme.palette.mode + ".svg"}
style={{...style}} />
{is_p2p === false &&
<WifiIcon style={{width: "20px", height: "20px", marginLeft: "-20px", marginBottom: "5px",
borderRadius: "10px", border: "1px solid",
backgroundColor: theme.palette.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>
)
}
@@ -4,7 +4,6 @@ import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import MythicTextField from './MythicTextField';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
@@ -13,11 +12,35 @@ import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import { toLocalTime } from '../utilities/Time';
import AceEditor from 'react-ace';
import "ace-builds/src-noconflict/mode-json";
import "ace-builds/src-noconflict/theme-github";
import "ace-builds/src-noconflict/theme-monokai";
import "ace-builds/src-noconflict/ext-searchbox";
import {useTheme} from '@mui/material/styles';
import WrapTextIcon from '@mui/icons-material/WrapText';
import {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) {
@@ -26,54 +49,160 @@ 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}
<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 = (name, value, error) => {
const onChange = (value) => {
setComment(value);
}
useEffect( () => {
setComment(props.value);
try{
setComment(JSON.stringify(JSON.parse(props.value), null, 2));
}catch(error){
setComment(props.value);
}
}, [props.value]);
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">{props.title}</DialogTitle>
<DialogContent dividers={true}>
<MythicTextField autoFocus onEnter={props?.onEnter || onCommitSubmit} onChange={onChange} value={comment} multiline={props?.multiline || false} maxRows={props.maxRows} />
{props.title !== "" &&
<MythicDraggableDialogTitle>{props.title}
<MythicStyledTooltip title={wrap ? "Toggle off word wrap" : "Toggl on word wrap"}
tooltipStyle={{float: "right"}}>
<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%"
fontSize={14}
showPrintMargin={false}
wrapEnabled={wrap}
value={comment}
focus={true}
onChange={onChange}
setOptions={{
tabSize: 4,
useWorker: false,
showInvisibles: false,
}}
/>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
Close
</Button>
<Button onClick={onCommitSubmit} variant="contained" color="success">
Submit
</Button>
</DialogActions>
{ (props.onClose || props.onSubmit) &&
<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>
);
}
@@ -92,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
@@ -145,14 +274,14 @@ 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>
<Paper elevation={5} style={{position: "relative"}} variant={"elevation"}>
<TableContainer className="mythicElement">
<MythicDraggableDialogTitle style={{wordBreak: "break-all", maxWidth: "100%"}}>{props.title}</MythicDraggableDialogTitle>
<TableContainer className="mythicElement" style={{paddingLeft: "10px"}}>
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
<TableHead>
<TableRow>
{headers.map( (header, index) => (
<TableCell key={'header' + index} style={index === 0 ? {width: "30%", wordBreak: "break-all"} : {wordBreak: "break-all"}}>{header}</TableCell>
<TableCell key={'header' + index} style={index === 0 ? {width: "15%", wordBreak: "break-all"} : {wordBreak: "break-all"}}>{header}</TableCell>
))}
</TableRow>
</TableHead>
@@ -168,7 +297,7 @@ export function MythicViewJSONAsTableDialog(props) {
<TableHead>
<TableRow>
{element.headers.map( (header, index) => (
<TableCell key={'eheader' + header + index} style={index === 0 ? {width: "30%", wordBreak: "break-all"} : {wordBreak: "break-all"}}>{header}</TableCell>
<TableCell key={'eheader' + header + index} style={index === 0 ? {width: "15%", wordBreak: "break-all"} : {wordBreak: "break-all"}}>{header}</TableCell>
))}
</TableRow>
</TableHead>
@@ -177,14 +306,14 @@ export function MythicViewJSONAsTableDialog(props) {
Object.keys(element.value).map( (key, dictIndex) => (
<TableRow key={'element' + dictIndex + "dictheader"}>
<TableCell style={{width: "30%", wordBreak: "break-all"}}>{key}</TableCell>
<TableCell style={{wordBreak: "break-all"}}>{convertValueToContextValue(key, element.value[key], props.me)}</TableCell>
<TableCell style={{wordBreak: "break-all", whiteSpace: "pre-wrap"}}>{convertValueToContextValue(key, element.value[key], props.me)}</TableCell>
</TableRow>
))
): (
element.value.map( (e, elementIndex) => (
<TableRow>
{element.headers.map( (header, headerIndex) => (
<TableCell key={'element' + elementIndex + "header" + headerIndex} style={headerIndex === 0 ? {width: "30%", wordBreak: "break-all"} : {wordBreak: "break-all"}}>{convertValueToContextValue(header, e[header], props.me)}</TableCell>
<TableCell key={'element' + elementIndex + "header" + headerIndex} style={headerIndex === 0 ? {width: "15%", wordBreak: "break-all"} : {wordBreak: "break-all", whiteSpace: "pre-wrap"}}>{convertValueToContextValue(header, e[header], props.me)}</TableCell>
))}
</TableRow>
))
@@ -212,7 +341,6 @@ export function MythicViewJSONAsTableDialog(props) {
</TableBody>
</Table>
</TableContainer>
</Paper>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
Close
@@ -1,25 +1,16 @@
import React from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import {MythicDialog} from './MythicDialog';
import MythicTextField from './MythicTextField';
import {MythicDialog, MythicModifyStringDialog} from './MythicDialog';
export function MythicDisplayTextDialog(props) {
return (
<MythicDialog fullWidth={props.fullWidth === null ? false : props.fullWidth} maxWidth={props.maxWidth === null ? "sm" : props.maxWidth} open={props.open} onClose={()=>{props.onClose()}} innerDialog={
<React.Fragment>
<DialogTitle id="form-dialog-title">{props.title}</DialogTitle>
<DialogContent dividers={true}>
<MythicTextField multiline={true} value={props.value} onChange={()=>{}} />
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
Close
</Button>
</DialogActions>
</React.Fragment>
<MythicDialog fullWidth={props.fullWidth === null ? false : props.fullWidth}
maxWidth={props.maxWidth === null ? "sm" : props.maxWidth}
open={props.open}
onClose={()=>{props.onClose()}} innerDialog={
<MythicModifyStringDialog title={props.title}
onClose={props.onClose}
value={props.value}
maxRows={20} />
} />
);
}
@@ -0,0 +1,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;
};
@@ -79,7 +79,7 @@ return (
Send a feedback report to the slack webhook configured for the current operation.
This provides a way to easily capture a bug, feedback requests, or comments without breaking operator flow too much. <br/>
Shift+Enter will auto-submit the form.
<TableContainer component={Paper} className="mythicElement">
<TableContainer className="mythicElement">
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
<TableBody style={{whiteSpace: "pre"}}>
<TableRow hover >
@@ -12,14 +12,8 @@ import ErrorIcon from '@mui/icons-material/Error';
import { useTheme } from '@mui/material/styles';
import { Typography } from '@mui/material';
import { MythicStyledTooltip } from "./MythicStyledTooltip";
import Grow from '@mui/material/Grow';
import Popper from '@mui/material/Popper';
import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import Paper from '@mui/material/Paper';
import WidgetsIcon from '@mui/icons-material/Widgets';
import ListSubheader from '@mui/material/ListSubheader';
import { areEqual } from 'react-window';
const PREFIX = 'FileBrowserVirtualTree';
@@ -145,22 +139,32 @@ const StyledAutoSizer = styled(AutoSizer)((
width: 1,
}
}));
const VirtualTreeRow = ({
function itemKey(index, data) {
// Find the item at the specified index.
// In this case "data" is an Array that was passed to List as "itemData".
const item = data[index];
if(item.root){
return `${item.group};${item.id}`;
}
if(item.is_group){
return item.group;
}
return `${item.group};${item.host};${item.full_path_text}`;
}
const VirtualTreeRow = React.memo(({
onSelectNode,
onExpandNode,
onCollapseNode,
onDoubleClickNode,
contextMenuOptions,
onContextMenu,
tabInfo,
selectedFolderData,
...ListProps
}) => {
const itemTreeData = ListProps.data[ListProps.index];
const item = ListProps.treeRootData[itemTreeData.group]?.[itemTreeData.host]?.[itemTreeData.full_path_text] || itemTreeData;
//console.log("item", item, "itemlookup", ListProps.treeRootData[itemTreeData.host]?.[itemTreeData.name])
const dropdownAnchorRef = React.useRef(null);
const theme = useTheme();
const handleOnClickButton = (e) => {
e.stopPropagation();
if (itemTreeData.isOpen) {
@@ -173,42 +177,25 @@ const VirtualTreeRow = ({
const handleOnClickRow = (e) => {
onSelectNode(item.id, {...item, group: itemTreeData.group, host: itemTreeData.host});
};
const [openContextMenu, setOpenContextMenu] = React.useState(false);
const handleContextClick = useCallback(
(event) => {
event.preventDefault();
event.stopPropagation();
if(item.root){
}else if(item.is_group){
}else {
if(contextMenuOptions && contextMenuOptions.length > 0){
setOpenContextMenu(true);
}
const handleContextClick = (e) => {
onContextMenu({event: e, item, itemTreeData});
}
const selectedPath = () => {
if(itemTreeData.group === selectedFolderData.group && itemTreeData.host === selectedFolderData.host){
if(itemTreeData.root){
return "selectedCallbackHierarchy";
}
if(selectedFolderData.full_path_text === itemTreeData.full_path_text){
return "selectedCallback";
}
},
[contextMenuOptions] // eslint-disable-line react-hooks/exhaustive-deps
);
const handleMenuItemClick = (event, index, callback_id, callback_display_id) => {
event.preventDefault();
event.stopPropagation();
contextMenuOptions[index].click({event,
node: {...item, group: itemTreeData.group, host: itemTreeData.host},
callback_id, callback_display_id
});
setOpenContextMenu(false);
};
const handleClose = (event) => {
if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
return;
}
setOpenContextMenu(false);
};
return "";
}
return (
<div className={"hoverme"} style={ListProps.style} onContextMenu={handleContextClick} ref={dropdownAnchorRef} onClick={handleOnClickRow}>
<div className={`hoverme ${selectedPath()}`}
style={ListProps.style}
onContextMenu={handleContextClick}
onClick={handleOnClickRow}>
<div style={{display: 'flex' , marginBottom: "1px", flexGrow: 1, width: "100%"}}>
{[...Array(itemTreeData.depth)].map((o, i) => (
<div
@@ -225,79 +212,52 @@ const VirtualTreeRow = ({
style={{ backgroundColor: theme.body, color: theme.text, alignItems: 'center', display: 'flex', paddingRight: "10px", textDecoration: itemTreeData.deleted ? 'line-through' : '' }}
>
<Popper open={openContextMenu} anchorEl={dropdownAnchorRef.current} role={undefined} transition style={{zIndex: 4}}>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement === 'bottom' ? 'left top' : 'left bottom',
}}
>
<Paper variant="outlined" className={"dropdownMenuColored"}>
<ClickAwayListener onClickAway={handleClose}>
<MenuList id="split-button-menu" style={{paddingTop: 0}} >
<ListSubheader component={"li"} className={"MuiListSubheader-root"}>
Act from current Callback: {tabInfo["displayID"]}
</ListSubheader>
{contextMenuOptions.map((option, index) => (
<MenuItem
key={option.name + index}
onClick={(event) => handleMenuItemClick(event, index, tabInfo["callbackID"], tabInfo["displayID"])}
>
{option.name}
</MenuItem>
))}
{
item?.callback && item?.["callback"]?.["id"] !== tabInfo["callbackID"] &&
<ListSubheader component={"li"} className={"MuiListSubheader-root"}>
Act from originating Callback: {item?.callback?.["display_id"] || tabInfo["displayID"]}
</ListSubheader>
}
{
item?.callback && item?.["callback"]?.["id"] !== tabInfo["callbackID"] &&
contextMenuOptions.map((option, index) => (
<MenuItem
key={option.name + index}
onClick={(event) => handleMenuItemClick(event, index,
item?.["callback"]?.["id"] || tabInfo["callbackID"],
item?.["callback"]?.["display_id"] || tabInfo["displayID"])}
>
{option.name}
</MenuItem>
))
}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
{itemTreeData.is_group ? (
<WidgetsIcon style={{marginLeft: "3px", marginRight: "5px" }} />
<WidgetsIcon style={{
width: "15px",
height: "15px",
marginLeft: "3px",
marginRight: "5px" }} />
): itemTreeData.root ? (
<ComputerIcon style={{ marginLeft: '3px', marginRight: '5px' }} />
<ComputerIcon style={{
width: "15px",
height: "15px",
marginLeft: '3px',
marginRight: '5px' }} />
) : !itemTreeData.can_have_children ? (
<DescriptionIcon style={{ marginLeft: '3px', marginRight: '5px' }} />
<DescriptionIcon style={{
width: "15px",
height: "15px",
marginLeft: '3px',
marginRight: '5px' }} />
) : itemTreeData.isOpen ? (
<FontAwesomeIcon
icon={faFolderOpen}
style={{
width: "15px",
height: "15px",
marginLeft: '3px',
marginRight: '5px',
color: item?.metadata?.has_children ? theme.folderColor : theme.palette.text.secondary,
}}
size={"1x"}
color: item?.has_children || item.success ? theme.folderColor : theme.emptyFolderColor,
}}
size={"lg"}
onClick={handleOnClickButton} />
) : (
<FontAwesomeIcon
style={{ paddingTop: '5px', marginLeft: '3px', marginRight: '5px', color: item?.metadata?.has_children ? theme.folderColor : theme.palette.text.secondary, }}
size={"lg"} icon={faFolder} onClick={handleOnClickButton} />
style={{
width: "15px",
height: "15px",
marginLeft: '3px',
marginRight: '5px',
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,
@@ -306,11 +266,11 @@ const VirtualTreeRow = ({
</Typography>
{item.success === true && itemTreeData.depth > 0 ? (
<MythicStyledTooltip title='Successfully listed contents of folder'>
<MythicStyledTooltip title='Successfully listed contents of folder' style={{display: "inline-flex", marginLeft: "5px"}}>
<CheckCircleOutlineIcon fontSize='small' color="success" />
</MythicStyledTooltip>
) : item.success === false && itemTreeData.depth > 0 ? (
<MythicStyledTooltip title='Failed to list contents of folder'>
<MythicStyledTooltip title='Failed to list contents of folder' style={{display: "inline-flex", marginLeft: "5px"}}>
<ErrorIcon fontSize='small' color="error" />
</MythicStyledTooltip>
) : null}
@@ -319,19 +279,28 @@ const VirtualTreeRow = ({
</div>
</div>
);
};
const FileBrowserVirtualTree = ({
}, areEqual);
const caseInsensitiveCompare = (a, b) => {
try{
return a.localeCompare(b);
}catch(error){
console.log("localeCompare failed for", a, b);
return a < b;
}
}
const FileBrowserVirtualTreePreMemo = ({
treeRootData,
treeAdjMatrix,
openNodes,
onSelectNode,
onExpandNode,
onCollapseNode,
contextMenuOptions,
onContextMenu,
showDeletedFiles,
selectedFolderData,
tabInfo,
}) => {
const gridRef = React.useRef(null);
const flattenNode = useCallback(
// node is just a full_path_text
(node, group, host, depth = 0) => {
@@ -339,26 +308,26 @@ const FileBrowserVirtualTree = ({
if(depth === 0){
return [
{
id: treeRootData[group][host][node].id,
name: treeRootData[group][host][node].name_text,
full_path_text: treeRootData[group][host][node].full_path_text,
deleted: treeRootData[group][host][node].deleted,
id: treeRootData[group]?.[host]?.[node].id,
name: treeRootData[group]?.[host]?.[node].name_text,
full_path_text: treeRootData[group]?.[host]?.[node].full_path_text,
deleted: treeRootData[group]?.[host]?.[node].deleted,
depth,
isLeaf: Object.keys(treeAdjMatrix[group][host]?.[node] || {}).length === 0,
can_have_children: treeRootData[group][host][node].can_have_children,
isLeaf: Object.keys(treeAdjMatrix[group]?.[host]?.[node] || {}).length === 0,
can_have_children: treeRootData[group]?.[host]?.[node].can_have_children,
isOpen: true,
children: (treeAdjMatrix[group][host]?.[node] || {}),
host,
group,
root: true
},
...(Object.keys(treeAdjMatrix[group][host]?.[node] || {})).reduce( (prev, cur) => {
...(Object.keys(treeAdjMatrix[group]?.[host]?.[node] || {})).sort(caseInsensitiveCompare).reduce( (prev, cur) => {
if(!treeRootData[group][host][cur].can_have_children){return [...prev]}
return [...prev, flattenNode(cur, group, host, depth+1)];
}, []).flat()
];
}
if (openNodes[treeRootData[group][host][node].id] === true) {
if (openNodes[`${group};${host};${treeRootData[group][host][node].full_path_text}`] === true) {
return [
{
id: treeRootData[group][host][node].id,
@@ -366,15 +335,15 @@ const FileBrowserVirtualTree = ({
full_path_text: treeRootData[group][host][node].full_path_text,
deleted: treeRootData[group][host][node].deleted,
depth,
isLeaf: Object.keys(treeAdjMatrix[group][host]?.[node] || {}).length === 0,
isLeaf: Object.keys(treeAdjMatrix[group]?.[host]?.[node] || {}).length === 0,
can_have_children: treeRootData[group][host][node].can_have_children,
isOpen: true,
children: (treeAdjMatrix[group][host]?.[node] || {}),
children: (treeAdjMatrix[group]?.[host]?.[node] || {}),
host,
group,
root: false,
},
...(Object.keys(treeAdjMatrix[group][host]?.[node] || {})).reduce( (prev, cur) => {
...(Object.keys(treeAdjMatrix[group]?.[host]?.[node] || {})).sort(caseInsensitiveCompare).reduce( (prev, cur) => {
if(!treeRootData[group][host][cur].can_have_children){return [...prev]}
if(!showDeletedFiles && treeRootData[group][host][cur].deleted){return [...prev]}
return [...prev, flattenNode(cur, group, host, depth+1)];
@@ -383,13 +352,13 @@ const FileBrowserVirtualTree = ({
}
return [
{
id: treeRootData[group][host][node].id,
name: treeRootData[group][host][node].name_text,
full_path_text: treeRootData[group][host][node].full_path_text,
deleted: treeRootData[group][host][node].deleted,
id: treeRootData[group]?.[host]?.[node].id,
name: treeRootData[group]?.[host]?.[node].name_text,
full_path_text: treeRootData[group]?.[host]?.[node].full_path_text,
deleted: treeRootData[group]?.[host]?.[node].deleted,
depth,
isLeaf: Object.keys(treeAdjMatrix[group]?.[host]?.[node] || {}).length === 0,
can_have_children: treeRootData[group][host][node].can_have_children,
can_have_children: treeRootData[group]?.[host]?.[node].can_have_children,
isOpen: false,
children: (treeAdjMatrix[group]?.[host]?.[node] || {}),
host,
@@ -399,15 +368,14 @@ const FileBrowserVirtualTree = ({
];
},
[openNodes, showDeletedFiles] // eslint-disable-line react-hooks/exhaustive-deps
[openNodes, showDeletedFiles, treeAdjMatrix] // eslint-disable-line react-hooks/exhaustive-deps
);
const flattenedNodes = useMemo(() => {
//console.log("in tree", treeRootData, treeAdjMatrix)
// need to return an array
let finalData = [];
//console.log(treeAdjMatrix);
const groupKeys = Object.keys(treeAdjMatrix).sort();
const groupKeys = Object.keys(treeAdjMatrix).sort(caseInsensitiveCompare);
for(let i = 0; i < groupKeys.length; i++){
finalData.push({
id: groupKeys[i],
@@ -424,7 +392,7 @@ const FileBrowserVirtualTree = ({
children: treeAdjMatrix[groupKeys[i]],
full_path_text: groupKeys[i],
});
const hostKeys = Object.keys(treeAdjMatrix[groupKeys[i]]).sort();
const hostKeys = Object.keys(treeAdjMatrix[groupKeys[i]]).sort(caseInsensitiveCompare);
for(let j = 0; j < hostKeys.length; j++){
//for(const [host, matrix] of Object.entries(hosts)){
finalData.push({
@@ -442,7 +410,7 @@ const FileBrowserVirtualTree = ({
full_path_text: hostKeys[j],
});
//console.log(matrix);
finalData.push(...Object.keys(treeAdjMatrix[groupKeys[i]][hostKeys[j]][""]).reduce((prev, c) => {
finalData.push(...Object.keys(treeAdjMatrix[groupKeys[i]][hostKeys[j]][""]).sort(caseInsensitiveCompare).reduce((prev, c) => {
if(!showDeletedFiles && c.deleted) {
return [...prev];
} else {
@@ -451,31 +419,44 @@ const FileBrowserVirtualTree = ({
}, []).flat())
}
}
//console.log("flattened data", finalData)
return finalData;
//nodes.map((node) => flattenNode(node)).flat()
},[flattenNode, treeRootData, treeAdjMatrix, showDeletedFiles]);
React.useEffect( () => {
let rowIndex = flattenedNodes?.findIndex(e =>
e.full_path_text === selectedFolderData.full_path_text &&
e.host === selectedFolderData.host &&
e.group === selectedFolderData.group
);
if(rowIndex >= 0){
if(gridRef.current){
gridRef.current?.scrollToItem(rowIndex, "smart")
}
}
}, [selectedFolderData, flattenedNodes]);
return flattenedNodes.length > 0 ? (
<StyledAutoSizer>
{(AutoSizerProps) => (
<List
itemData={flattenedNodes}
direction="vertical"
layout="vertical"
height={AutoSizerProps.height}
width={AutoSizerProps.width}
itemCount={flattenedNodes.length}
itemKey={itemKey}
itemSize={24}
ref={gridRef}
>
{(ListProps) => (
<VirtualTreeRow
{...ListProps}
tabInfo={tabInfo}
selectedFolderData={selectedFolderData}
treeRootData={treeRootData}
onSelectNode={onSelectNode}
onExpandNode={onExpandNode}
onCollapseNode={onCollapseNode}
contextMenuOptions={contextMenuOptions}
onContextMenu={onContextMenu}
/>
)}
</List>
@@ -483,5 +464,4 @@ const FileBrowserVirtualTree = ({
</StyledAutoSizer>
) : null;
};
export default FileBrowserVirtualTree;
export const FileBrowserVirtualTree = React.memo(FileBrowserVirtualTreePreMemo);
@@ -1,122 +1,100 @@
import React from 'react';
import {previewFileQuery} from "../pages/Search/FileMetaTable";
import {snackActions} from "../utilities/Snackbar";
import {MythicStyledTooltip} from "./MythicStyledTooltip";
import hexFile from "../../assets/file_bin.png";
import txtFile from "../../assets/file_txt.png";
import {MythicDialog} from "./MythicDialog";
import {PreviewFileStringDialog} from "../pages/Search/PreviewFileStringDialog";
import {PreviewFileHexDialog} from "../pages/Search/PreviewFileHexDialog";
import {Link} from '@mui/material';
import {useMutation, useLazyQuery, gql} from '@apollo/client';
import {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,
filename: filename,
})
const [openPreviewStringsDialog, setOpenPreviewStringsDialog] = React.useState(false);
const [openPreviewHexDialog, setOpenPreviewHexDialog] = React.useState(false);
const [fileContents, setFileContents] = React.useState('');
const [previewFileString] = useMutation(previewFileQuery, {
onCompleted: (data) => {
if(data.previewFile.status === "success"){
setFileContents(data.previewFile.contents);
setOpenPreviewStringsDialog(true);
}else{
snackActions.error(data.previewFile.error)
}
},
onError: (data) => {
console.log(data);
snackActions.error(data);
}
});
const [previewFileHex] = useMutation(previewFileQuery, {
onCompleted: (data) => {
if(data.previewFile.status === "success"){
setFileContents(data.previewFile.contents);
setOpenPreviewHexDialog(true);
}else{
snackActions.error(data.previewFile.error)
}
},
onError: (data) => {
console.log(data);
snackActions.error(data);
}
});
const onPreviewStrings = (event) => {
const [openPreviewMediaDialog, setOpenPreviewMediaDialog] = React.useState(false);
const onPreviewMedia = (event) => {
if(event){
event.preventDefault();
event.stopPropagation();
}
previewFileString({variables: {file_id: fileData.agent_file_id}})
setOpenPreviewMediaDialog(true);
}
const onPreviewHex = (event) => {
if(event){
event.preventDefault();
event.stopPropagation();
const getFileInformationSuccess = (data) => {
if(data.filemeta.length === 0){
setFileData({
filename: undefined,
display_link: "",
agent_file_id: agent_file_id,
})
return;
}
setFileData( {...fileData,
agent_file_id: data.filemeta[0].agent_file_id,
filename: b64DecodeUnicode(data.filemeta[0].filename_text)});
if(display_link === "" || display_link === undefined){
setFileData( {...fileData,
agent_file_id: data.filemeta[0].agent_file_id,
filename: b64DecodeUnicode(data.filemeta[0].filename_text),
display_link: b64DecodeUnicode(data.filemeta[0].filename_text)});
}
previewFileHex({variables: {file_id: fileData.agent_file_id}})
}
const [getFileInformation] = useLazyQuery(getfileInformationQuery, {
onCompleted: (data) => {
setFileData( {...fileData, filename: b64DecodeUnicode(data.filemeta[0].filename_text)});
if(display_link === "" || display_link === undefined){
setFileData( {...fileData,
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 getFileInformationError = (data) => {
snackActions.error("Failed to fetch file data: " + data);
console.log(data);
}
const getFileInformation = useMythicLazyQuery(getfileInformationQuery, {
fetchPolicy: "no-cache"
})
React.useEffect( () => {
if(filename === "" || filename === undefined){
getFileInformation({variables: {file_id: fileData.agent_file_id}})
if(agent_file_id !== '' && agent_file_id !== undefined){
getFileInformation({variables: {file_id: agent_file_id}})
.then(({data}) => getFileInformationSuccess(data)).catch((data) => getFileInformationError(data));
}
}, [filename]);
}, [filename, agent_file_id]);
if(agent_file_id === '' || agent_file_id === undefined){
return null
}
if(fileData.agent_file_id === '' || fileData.filename === undefined){
return agent_file_id
}
const onClose = (e) => {
if(e){
e.preventDefault();
e.stopPropagation();
}
setOpenPreviewMediaDialog(false)
}
return (
<>
<MythicStyledTooltip title={"Preview HEX XXD"}>
<img src={hexFile} alt={"preview hex"} style={{height: "35px", cursor: "pointer"}}
onClick={onPreviewHex}/>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Preview Strings"}>
<img src={txtFile} alt={"preview strings"} style={{height: "35px", cursor: "pointer"}}
onClick={onPreviewStrings} />
<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}>
{fileData.display_link === "" ? window.location.origin + "/direct/download/" + fileData.agent_file_id : fileData.display_link}
</Link>
{openPreviewStringsDialog &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openPreviewStringsDialog}
onClose={()=>{setOpenPreviewStringsDialog(false);}}
innerDialog={<PreviewFileStringDialog onClose={()=>{setOpenPreviewStringsDialog(false);}}
filename={fileData.filename} contents={fileContents}
/>}
/>
}
{openPreviewHexDialog &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openPreviewHexDialog}
onClose={()=>{setOpenPreviewHexDialog(false);}}
innerDialog={<PreviewFileHexDialog onClose={()=>{setOpenPreviewHexDialog(false);}}
filename={fileData.filename} contents={fileContents}
/>}
{openPreviewMediaDialog &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openPreviewMediaDialog}
onClose={onClose}
innerDialog={<PreviewFileMediaDialog
agent_file_id={fileData.agent_file_id}
filename={fileData.filename}
onClose={onClose} />}
/>
}
</>
@@ -1,24 +1,28 @@
import { snackActions } from '../utilities/Snackbar';
import {snackActions} from '../utilities/Snackbar';
export const UploadTaskFile = async (file, comment) => {
let formData = new FormData();
formData.append("file", file);
formData.append("comment", comment);
snackActions.info("Uploading " + file.name + " to Mythic...", {autoHideDuration: 1000});
try{
formData.append("file", file);
formData.append("comment", comment);
snackActions.info("Uploading " + file.name + " to Mythic...", {autoHideDuration: 1000});
}catch(error){
console.log(error)
return null;
}
try{
const upload_response = await fetch('/api/v1.4/task_upload_file_webhook', {
method: 'POST',
body: formData,
headers: {
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
"Authorization": `Bearer ${localStorage.getItem("access_token")}`,
MythicSource: "web",
}
});
try{
const upload_result = upload_response.json().then(data => {
//console.log(data);
return data?.agent_file_id || data?.error || null;
}).catch(error => {
console.log(upload_response);
snackActions.warning("Error: " + upload_response.statusText + "\nError Code: " + upload_response.status);
console.log("Error trying to get json response", error.toString());
return null;
@@ -32,4 +36,68 @@ export const UploadTaskFile = async (file, comment) => {
snackActions.error(error.toString());
return null;
}
}
export const UploadEventFile = async (file, comment) => {
let formData = new FormData();
formData.append("file", file);
formData.append("comment", comment);
snackActions.info("Uploading " + file.name + " to Mythic...", {autoHideDuration: 1000});
try{
const upload_response = await fetch('/api/v1.4/eventing_import_webhook', {
method: 'POST',
body: formData,
headers: {
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
}
});
try{
return upload_response.json().then(data => {
//console.log(data);
return data;
}).catch(error => {
console.log(upload_response);
snackActions.warning("Error: " + upload_response.statusText + "\nError Code: " + upload_response.status);
console.log("Error trying to get json response", error.toString());
return null;
});
}catch(error){
snackActions.error(error.toString());
return null;
}
}catch(error){
snackActions.error(error.toString());
return null;
}
}
export const UploadEventGroupFile = async (file, eventgroup_id) => {
let formData = new FormData();
formData.append("eventgroup_id", eventgroup_id);
formData.append("file", file);
snackActions.info("Uploading " + file.name + " to Mythic...", {autoHideDuration: 1000});
try{
const upload_response = await fetch('/api/v1.4/eventing_register_file_webhook', {
method: 'POST',
body: formData,
headers: {
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
}
});
try{
return upload_response.json().then(data => {
//console.log(data);
return data;
}).catch(error => {
console.log(upload_response);
snackActions.warning("Error: " + upload_response.statusText + "\nError Code: " + upload_response.status);
console.log("Error trying to get json response", error.toString());
return null;
});
}catch(error){
snackActions.error(error.toString());
return null;
}
}catch(error){
snackActions.error(error.toString());
return null;
}
}
@@ -0,0 +1,257 @@
import * as React from "react";
import styled from "@emotion/styled";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import ArrowRight from "@mui/icons-material/ArrowRight";
// slightly modified from https://medium.com/geekculture/creating-a-dropdown-with-nested-menu-items-using-react-mui-bb0c084226da
export const Dropdown = React.forwardRef(
(
{
menu,
isOpen: controlledIsOpen,
onOpen: onControlledOpen,
externallyOpen,
minWidth,
absoluteX,
absoluteY,
anchorReference="anchorEl",
style,
transformOrigin,
anchorOrigin,
},
ref
) => {
const [isInternalOpen, setInternalOpen] = React.useState(null);
const isOpen = controlledIsOpen || isInternalOpen;
let anchorRef = React.useRef(isOpen);
if (ref) {
anchorRef = ref;
}
const handleClose = (event) => {
event.stopPropagation();
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
handleForceClose();
};
const handleForceClose = () => {
onControlledOpen ? onControlledOpen(null) : setInternalOpen(null);
};
const renderMenu = (menuItem, index) => {
const { ...props } = menuItem.props;
let extraProps = {};
if (props.menu) {
extraProps = {
parentMenuOpen: isOpen
};
}
return React.createElement(menuItem.type, {
...props,
key: index,
...extraProps,
onClick: (event) => {
event.stopPropagation();
if (menuItem.props.onClick) {
menuItem.props.onClick(event);
}
},
children: props.menu
? React.Children.map(props.menu, renderMenu)
: props.children
});
};
const anchorEl = isOpen && isOpen.currentTarget ? isOpen.currentTarget : isOpen;
return (
<>
<Menu
elevation={5}
PaperProps={{ sx: { minWidth: minWidth ?? 0 } }}
style={{zIndex: 100000, position: "absolute"}}
anchorEl={anchorReference === "anchorEl" ? anchorEl : undefined}
transition={"true"}
open={!!externallyOpen}
anchorPosition={anchorReference === "anchorEl" ? undefined : {top: absoluteY, left: absoluteX}}
anchorReference={ anchorReference ? anchorReference : "anchorEl"}
onClose={handleClose}
anchorOrigin={anchorOrigin ? anchorOrigin : {
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={transformOrigin ? transformOrigin : {
vertical: 'top',
horizontal: 'left',
}}
>
{React.Children.map(menu, renderMenu)}
</Menu>
</>
);
}
);
const NestedMenuItem = React.forwardRef((props, ref) => {
const {
parentMenuOpen,
label,
rightIcon = <ArrowRight style={{ fontSize: 16 }} />,
children,
customTheme,
className,
tabIndex: tabIndexProp,
ContainerProps: ContainerPropsProp = {},
rightAnchored,
...MenuItemProps
} = props;
const { ref: containerRefProp, ...ContainerProps } = ContainerPropsProp;
const menuItemRef = React.useRef(null);
React.useImperativeHandle(ref, () => menuItemRef.current);
const containerRef = React.useRef(null);
React.useImperativeHandle(containerRefProp, () => containerRef.current);
const menuContainerRef = React.useRef(null);
const [isSubMenuOpen, setIsSubMenuOpen] = React.useState(false);
const handleMouseEnter = (event) => {
setIsSubMenuOpen(true);
if (ContainerProps?.onMouseEnter) {
ContainerProps.onMouseEnter(event);
}
};
const handleMouseLeave = (event) => {
setIsSubMenuOpen(false);
if (ContainerProps?.onMouseLeave) {
ContainerProps.onMouseLeave(event);
}
};
const isSubmenuFocused = () => {
const active = containerRef.current?.ownerDocument?.activeElement;
for (const child of menuContainerRef.current?.children ?? []) {
if (child === active) {
return true;
}
}
return false;
};
const handleFocus = (event) => {
if (event.target === containerRef.current) {
setIsSubMenuOpen(true);
}
if (ContainerProps?.onFocus) {
ContainerProps.onFocus(event);
}
};
const handleKeyDown = (event) => {
if (event.key === "Escape") {
setIsSubMenuOpen(false);
return;
}
if (isSubmenuFocused()) {
event.stopPropagation();
}
const active = containerRef.current?.ownerDocument?.activeElement;
if (event.key === "ArrowLeft" && isSubmenuFocused()) {
containerRef.current?.focus();
}
if (
event.key === "ArrowRight" &&
event.target === containerRef.current &&
event.target === active
) {
const firstChild = menuContainerRef.current?.children[0];
firstChild?.focus();
}
if(event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "ArrowLeft"){
setIsSubMenuOpen(false);
}
};
const open = isSubMenuOpen && parentMenuOpen;
let tabIndex;
if (!props.disabled) {
tabIndex = tabIndexProp !== undefined ? tabIndexProp : -1;
}
return (
<div
{...ContainerProps}
ref={containerRef}
onFocus={handleFocus}
tabIndex={tabIndex}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onKeyDown={handleKeyDown}
>
<MenuItem
{...MenuItemProps}
data-open={!!open || undefined}
className={className}
ref={menuItemRef}
>
{label}
<div style={{ flexGrow: 1 }} />
{rightIcon}
</MenuItem>
<Menu
hideBackdrop
disablePortal
disableEnforceFocus={false}
disableAutoFocus={false}
style={{ pointerEvents: "none", zIndex: 100000}}
anchorEl={menuItemRef.current}
anchorOrigin={{
vertical: "top",
horizontal: rightAnchored ? "left" : "right"
}}
transformOrigin={{
vertical: "top",
horizontal: rightAnchored ? "right" : "left"
}}
css={customTheme}
open={!!open}
onClose={() => {
setIsSubMenuOpen(false);
}}
MenuListProps={{
'aria-hidden': false,
role: 'menu'
}}
>
<div ref={menuContainerRef} style={{ pointerEvents: "auto" }}>
{children}
</div>
</Menu>
</div>
);
});
export const DropdownMenuItem = styled(MenuItem)`
`;
export const DropdownNestedMenuItem = styled(NestedMenuItem)`
`;
@@ -0,0 +1,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>
)
}
@@ -1,27 +1,34 @@
import React, { useCallback } from 'react';
import {classes} from './styles';
import Grow from '@mui/material/Grow';
import Popper from '@mui/material/Popper';
import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import Paper from '@mui/material/Paper';
import {useTheme} from '@mui/material/styles';
import {Dropdown, DropdownMenuItem, DropdownNestedMenuItem} from "../MythicNestedMenus";
const CellPreMemo = ({ VariableSizeGridProps: { style, rowIndex, columnIndex, data } }) => {
const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
const [openContextMenu, setOpenContextMenu] = React.useState(false);
const rowClassName = data.gridUUID + "row" + rowIndex;
const contextMenuOptions = data?.rowContextMenuOptions || [];
const rowHighlight = rowIndex % 2 === 1 ? 'MythicResizableGridRowHighlight' : '';
const [contextMenuOptions, setContextMenuOptions] = React.useState(data?.rowContextMenuOptions || []);
const dropdownAnchorRef = React.useRef(null);
const handleDoubleClick = useCallback(
(e) => {
data.onDoubleClickRow(e, rowIndex - 1); // minus 1 to account for header row
},
[data, rowIndex]
);
const item = data.items[rowIndex][columnIndex];
const cellStyle = item?.props?.cellData?.cellStyle || {};
const rowStyle = data.items[rowIndex][columnIndex]?.props?.rowData?.rowStyle || {};
const contextMenuLocationRef = React.useRef({x: 0, y: 0});
const handleDoubleClick = useCallback(
(e) => {
e.preventDefault();
e.stopPropagation();
data.onDoubleClickRow(e, rowIndex - 1, data.items[rowIndex][columnIndex]?.props?.rowData); // minus 1 to account for header row
},
[data, rowIndex]
);
const handleClick = (event) => {
event.preventDefault();
event.stopPropagation();
if(data.onRowClick){
data.onRowClick({event, rowDataStatic: data.items[rowIndex][columnIndex]?.props?.rowData})
}
};
const selectedClass = data.items[rowIndex][columnIndex]?.props?.rowData?.selected ? "selectedCallback" : "";
const onMouseEnter = () => {
const cells = document.getElementsByClassName(rowClassName);
if(cells.length > 0){
@@ -38,54 +45,120 @@ const CellPreMemo = ({ VariableSizeGridProps: { style, rowIndex, columnIndex, da
}
}
}
const handleMenuItemClick = (event, index) => {
contextMenuOptions[index].click({event, columnIndex, rowIndex, data: data.items[rowIndex][columnIndex]?.props?.rowData || {}});
const handleMenuItemClick = (event, clickOption) => {
event.preventDefault();
event.stopPropagation();
clickOption({event, columnIndex, rowIndex, data: data.items[rowIndex][columnIndex]?.props?.rowData || {}});
setOpenContextMenu(false);
};
React.useEffect( () => {
if(!openContextMenu){
onMouseLeave();
}
}, [openContextMenu]);
const handleContextClick = useCallback(
(event) => {
event.preventDefault();
if(item.disableFilterMenu){
event.stopPropagation();
if(item?.disableFilterMenu){
return;
}
if(contextMenuOptions && contextMenuOptions.length > 0){
setOpenContextMenu(true);
contextMenuLocationRef.current.x = event.clientX;
contextMenuLocationRef.current.y = event.clientY;
if(data.onRowContextMenuClick){
const newMenuItems = data.onRowContextMenuClick({rowDataStatic: data.items[rowIndex][columnIndex]?.props?.rowData});
Promise.resolve(newMenuItems).then(function(value) {
if(value.length > 0){
setContextMenuOptions(value);
setOpenContextMenu(true);
}
})
} else {
if(contextMenuOptions && contextMenuOptions.length > 0){
setOpenContextMenu(true);
}
}
},
[contextMenuOptions] // eslint-disable-line react-hooks/exhaustive-deps
[contextMenuOptions, data.onRowContextMenuClick] // eslint-disable-line react-hooks/exhaustive-deps
);
return (
<div style={{...style, ...cellStyle, ...rowStyle}}
className={`${classes.cell} ${rowClassName}`}
className={`${classes.cell} ${rowClassName} ${rowHighlight} ${selectedClass}`}
onDoubleClick={handleDoubleClick}
onClick={handleClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onContextMenu={handleContextClick}
ref={dropdownAnchorRef}
>
<div className={classes.cellInner}>
<div className={classes.cellInner} style={{height: style.height}}>
{item}
</div>
<ContextMenu dropdownAnchorRef={dropdownAnchorRef} contextMenuOptions={contextMenuOptions}
disableFilterMenu={item.disableFilterMenu} openContextMenu={openContextMenu}
disableFilterMenu={item?.disableFilterMenu} openContextMenu={openContextMenu}
contextMenuLocationRef={contextMenuLocationRef}
setOpenContextMenu={setOpenContextMenu} handleMenuItemClick={handleMenuItemClick}
/>
</div>
);
};
const ContextMenu = ({openContextMenu, dropdownAnchorRef, contextMenuOptions, setOpenContextMenu, handleMenuItemClick}) => {
const theme = useTheme();
export const ContextMenu = ({openContextMenu, dropdownAnchorRef, contextMenuOptions, setOpenContextMenu, handleMenuItemClick, contextMenuLocationRef}) => {
const handleClose = (event) => {
if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
return;
}
//if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
// return;
//}
setOpenContextMenu(false);
};
return (
openContextMenu &&
<Popper open={openContextMenu} anchorEl={dropdownAnchorRef.current} role={undefined} transition disablePortal style={{zIndex: 4}}>
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
<Dropdown
isOpen={dropdownAnchorRef.current}
onOpen={setOpenContextMenu}
externallyOpen={openContextMenu}
absoluteX={contextMenuLocationRef.current.x}
absoluteY={contextMenuLocationRef.current.y}
anchorReference={"anchorPosition"}
menu={[
contextMenuOptions.map((option, index) => (
option.type === 'item' ? (
<DropdownMenuItem
key={option.name}
disabled={option.disabled}
onClick={(event) => handleMenuItemClick(event, option.click)}
>
{option.icon}{option.name}
</DropdownMenuItem>
) : option.type === 'menu' ? (
<DropdownNestedMenuItem
label={option.name}
disabled={option.disabled}
menu={
option.menuItems.map((menuOption, indx) => (
<DropdownMenuItem
key={menuOption.name}
disabled={menuOption.disabled}
onClick={(event) => handleMenuItemClick(event, menuOption.click)}
>
{menuOption.icon}{menuOption.name}
</DropdownMenuItem>
))
}
/>
) : null
)),
]}
/>
</ClickAwayListener>
)
}
const Cell = React.memo(CellPreMemo);
export default Cell;
/*
<Popper open={openContextMenu} anchorEl={dropdownAnchorRef.current} role={undefined} transition disablePortal style={{zIndex: 4}}>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
@@ -110,8 +183,4 @@ const ContextMenu = ({openContextMenu, dropdownAnchorRef, contextMenuOptions, se
</Grow>
)}
</Popper>
)
}
const Cell = React.memo(CellPreMemo);
export default Cell;
*/
@@ -1,9 +1,12 @@
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(false);
const [isDragging, setIsDragging] = useState(-1);
const nodeRef = React.useRef(null);
return (
<div
ref={ref}
@@ -11,13 +14,15 @@ const DraggableHandles = React.forwardRef(({ height, rowHeight, width, minColumn
style={{
height: height,
width: width,
pointerEvents: isDragging ? 'initial' : 'none',
pointerEvents: isDragging >= 0 ? 'initial' : 'none',
}}>
{columnWidths.map((_, i) => {
// leftOffset is the sum of the width of all columns left of i
const leftOffset = columnWidths.slice(0, i).reduce((a, b) => a + b, 0);
return (
<Draggable
key={i}
nodeRef={nodeRef}
axis='x'
bounds={{
left: minColumnWidth - columnWidths[i],
@@ -25,28 +30,25 @@ const DraggableHandles = React.forwardRef(({ height, rowHeight, width, minColumn
top: 0,
bottom: 0,
}}
position={isDragging ? null : { x: 0, y: 0 }}
offsetParent={document.body}
defaultPosition={{x: 0, y: 0}}
position={{x: 0, y: 0}}
onStart={() => {
setIsDragging(i);
}}
onStop={(e, data) => {
setIsDragging(false);
setIsDragging(-1);
onStop(data.x, i);
}}>
<div
className={classes.draggableHandlesClickArea}
style={{
left: leftOffset + columnWidths[i] - 1 - 8,
height: rowHeight,
}}>
<div
className={classes.draggableHandlesIndicator}
<DragHandleIcon
ref={nodeRef}
className={isDragging === i ? classes.draggableHandlesClickAreaSelected : classes.draggableHandlesClickArea}
style={{
display: isDragging === i ? 'block' : 'none',
height: height / 2,
}}
/>
</div>
left: leftOffset + columnWidths[i] - 1 - 7,
rotate: isDragging === i ? "0deg" : "90deg",
}}>
</DragHandleIcon>
</Draggable>
);
})}
@@ -54,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, color: "white"}}>
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
<MenuList id="split-button-menu" >
{contextMenuOptions.map((option, index) => (
<MenuItem
key={option.name + index}
onClick={(event) => handleMenuItemClick(event, index)}
>
{option.name}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
<ContextMenu dropdownAnchorRef={dropdownAnchorRef} contextMenuOptions={contextMenuOptions}
disableFilterMenu={item?.disableFilterMenu} openContextMenu={openContextMenu}
contextMenuLocationRef={contextMenuLocationRef}
setOpenContextMenu={setOpenContextMenu} handleMenuItemClick={handleMenuItemClick}
/>
</Box>
</div>
);
@@ -7,17 +7,26 @@ import HeaderCell from './HeaderCell';
import Cell from './Cell';
import DraggableHandles from './DraggableHandles';
import {classes} from './styles';
import {GetMythicSetting, useSetMythicSetting} from "../MythicSavedUserSetting";
const HeaderCellContext = createContext({});
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}
@@ -31,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}
@@ -60,10 +66,11 @@ const innerElementType = React.forwardRef(({ children, ...rest }, ref) => {
);
});
const getShortRandomString = () => {
export const GetShortRandomString = () => {
return (Math.random() + 1).toString(36).substring(2);
}
const ResizableGridWrapper = ({
name,
columns,
sortIndicatorIndex,
sortDirection,
@@ -73,17 +80,31 @@ const ResizableGridWrapper = ({
onDoubleClickRow,
contextMenuOptions,
rowContextMenuOptions,
onRowContextMenuClick,
rowHeight,
headerRowHeight,
widthMeasureKey,
callbackTableGridRef,
onRowClick,
...AutoSizerProps
}) => {
/* Hooks */
const { width: scrollbarWidth } = useScrollbarSize();
const [columnWidths, setColumnWidths] = useState(columns.map((column) => column.width || MIN_COLUMN_WIDTH));
const gridUUID = React.useMemo( () => getShortRandomString(), []);
const localColumnsRef = React.useRef(columns);
const initialSavedWidths = name === undefined ? [] : GetMythicSetting({setting_name: `${name}_column_widths`, default_value: {}, output: "json-array"});
const [columnWidths, setColumnWidths] = useState(columns.map((column, index) => {
if(initialSavedWidths.length > 0){
return initialSavedWidths[index];
}
if(column.fillWidth){
return Math.max(column?.width || 0, MIN_FLEX_COLUMN_WIDTH);
}
return Math.max(column?.width || 0, MIN_COLUMN_WIDTH);
}));
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;
@@ -92,16 +113,24 @@ const ResizableGridWrapper = ({
);
const getRowHeight = useCallback(
(index) => {
if(index === 0){
return headerRowHeight;
}
return rowHeight;
},
[rowHeight]
[rowHeight, headerRowHeight]
);
useEffect(() => {
if(initialSavedWidths.length > 0 && localColumnsRef.current.length === columns.length){
return;
}
localColumnsRef.current = columns;
const totalWidth = AutoSizerProps.width - scrollbarWidth;
const updatedColumnWidths = columns.map((column) => column.width || MIN_COLUMN_WIDTH);
const updatedColumnWidths = columns.map((column, index) => {
return column.width || MIN_COLUMN_WIDTH
});
const totalWidthDiff = totalWidth - updatedColumnWidths.reduce((a, b) => a + b, 0);
if (totalWidthDiff > 0) {
if (totalWidthDiff !== 0) {
let updatedWidthIndexs = [];
for(let i = 0; i < columns.length; i++){
// check if any of the columns have the `fillWidth` property to true
@@ -113,46 +142,67 @@ const ResizableGridWrapper = ({
updatedWidthIndexs.push(columns.length - 1);
}
for(let i = 0; i < updatedWidthIndexs.length; i++){
updatedColumnWidths[updatedWidthIndexs[i]] += totalWidthDiff / updatedWidthIndexs.length;
updatedColumnWidths[updatedWidthIndexs[i]] += Math.max(totalWidthDiff / updatedWidthIndexs.length, MIN_FLEX_COLUMN_WIDTH);
}
//updatedColumnWidths[updatedWidthIndex] += totalWidthDiff;
}
setColumnWidths(updatedColumnWidths);
}, [scrollbarWidth, columns, AutoSizerProps.width]); // eslint-disable-line react-hooks/exhaustive-deps
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 updatedWidths = columnWidths.map((columnWidth, index) => {
const resizeColumn = useCallback( (x, columnIndex) => {
const updatedColumnWidths = columnWidths.map((columnWidth, index) => {
if (columnIndex === index) {
return Math.floor(Math.max(columnWidth + x, MIN_COLUMN_WIDTH));
}
return Math.floor(columnWidth);
});
setColumnWidths(updatedWidths);
};
setColumnWidths(updatedColumnWidths);
if(name !== undefined){
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
}
const autosizeColumn = ({columnIndex}) => {
}, [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;
}
if(columns[columnIndex].key === "mythictree_groups"){
return itemRow[columnIndex]?.props?.cellData.length;
}
if(columns[columnIndex].type === "size"){
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;
}
try{
items = JSON.parse(itemRow[columnIndex]?.props?.rowData?.[columns[columnIndex].key]);
if(Array.isArray(items) && items.length > 0){
@@ -161,18 +211,26 @@ 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(!data){
return 3;
if(columns[columnIndex].inMetadata){
return itemRow[columnIndex]?.props?.cellData.length;
}
if(data === undefined){
return MIN_COLUMN_WIDTH;
}
if(data.plaintext){
return String(data.plaintext).length || -1;
return String(data.plaintext)?.length;
} else if(data?.button?.name) {
return String(data?.button?.name)?.length ;
} else {
return String(data).length || -1;
return MIN_COLUMN_WIDTH;
}
//return String(itemRow[columnIndex]?.props?.rowData?.[columns[columnIndex].key]).length || -1;
} else if(typeof(itemRow[columnIndex]?.props?.cellData) === "string") {
try {
items = JSON.parse(itemRow[columnIndex]?.props?.cellData);
if (Array.isArray(items) && items.length > 0) {
@@ -188,15 +246,33 @@ const ResizableGridWrapper = ({
}
}));
const updatedWidths = columnWidths.map((columnWidth, index) => {
const updatedColumnWidths = columnWidths.map((columnWidth, index) => {
if (columnIndex === index) {
if(isNaN(longestElementInColumn)){
return MIN_COLUMN_WIDTH;
}
return Math.floor(Math.max(longestElementInColumn * 10 + 40, MIN_COLUMN_WIDTH));
}
return Math.floor(columnWidth);
});
setColumnWidths(updatedWidths);
};
//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){
callbackTableGridRef.current = gridRef.current;
}
}, [gridRef.current])
const itemsWithHeader = [columns, ...items];
const headerCellData = {
"getRowHeight": getRowHeight,
@@ -210,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}>
@@ -222,20 +304,20 @@ const ResizableGridWrapper = ({
headerNameKey={headerNameKey}
rowCount={itemsWithHeader.length}
rowHeight={getRowHeight}
itemData={{ items: itemsWithHeader, onDoubleClickRow, gridUUID, rowContextMenuOptions}}
itemData={{ items: itemsWithHeader,
onDoubleClickRow, gridUUID,
rowContextMenuOptions, onRowClick,
onRowContextMenuClick}}
innerElementType={innerElementType}
overscanRowCount={5}
onScroll={({ scrollLeft }) => {
if (dragHandlesRef.current) {
dragHandlesRef.current.scrollTo({ left: scrollLeft });
}
}}
overscanRowCount={20}
useIsScrolling={false}
onScroll={localOnScroll}
ref={gridRef}>
{CellRenderer}
</VariableSizeGrid>
<DraggableHandles
height={AutoSizerProps.height}
rowHeight={getRowHeight(0)}
rowHeight={headerRowHeight}
width={AutoSizerProps.width}
minColumnWidth={MIN_COLUMN_WIDTH}
columnWidths={columnWidths}
@@ -258,24 +340,34 @@ const MythicResizableGrid = ({
onDoubleClickRow,
contextMenuOptions,
rowContextMenuOptions,
onRowContextMenuClick,
widthMeasureKey,
rowHeight = 32,
rowHeight = 20,
headerRowHeight = 20,
callbackTableGridRef,
onRowClick,
name,
}) => {
return (
<AutoSizer style={{height: "100%"}}>
{(AutoSizerProps) => (
<ResizableGridWrapper
name={name}
columns={columns}
callbackTableGridRef={callbackTableGridRef}
headerNameKey={headerNameKey}
sortIndicatorIndex={sortIndicatorIndex}
sortDirection={sortDirection}
items={items}
widthMeasureKey={widthMeasureKey}
rowHeight={rowHeight}
headerRowHeight={headerRowHeight}
onClickHeader={onClickHeader}
onDoubleClickRow={onDoubleClickRow}
contextMenuOptions={contextMenuOptions}
rowContextMenuOptions={rowContextMenuOptions}
onRowContextMenuClick={onRowContextMenuClick}
onRowClick={onRowClick}
{...AutoSizerProps}
/>
)}
@@ -300,6 +392,7 @@ MythicResizableGrid.propTypes = {
contextMenuOptions: PropTypes.array,
rowContextMenuOptions: PropTypes.array,
rowHeight: PropTypes.number,
headerRowHeight: PropTypes.number,
headerNameKey: PropTypes.string,
widthMeasureKey: PropTypes.string
};
@@ -7,5 +7,6 @@ export const classes = {
cellInner: `${PREFIX}-cellInner`,
draggableHandlesContainer: `${PREFIX}-draggableHandlesContainer`,
draggableHandlesClickArea: `${PREFIX}-draggableHandlesClickArea`,
draggableHandlesClickAreaSelected: `${PREFIX}-draggableHandlesClickAreaSelected`,
draggableHandlesIndicator: `${PREFIX}-draggableHandlesIndicator`,
}
@@ -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
@@ -9,42 +24,60 @@ setting_name options:
showHostname
showCallbackGroups
showMedia
*/
export function useMythicSetting({setting_name, default_value, output="boolean"}){
const me = useReactiveVar(meState);
// get the initial value we have stored
interactType
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;
default:
console.log("unknown output type", output);
}
callbacks_table_columns
callbacks_table_filters
*/
export function useGetMythicSetting({setting_name, default_value}){
const preferences = useReactiveVar(mePreferences);
let initialStorageSetting = GetMythicSetting({setting_name, default_value});
const [setting, setSetting] = React.useState(initialStorageSetting);
React.useEffect( () => {
// update the initial value if the user changes
const localStorageSetting = localStorage.getItem(`${me?.user?.user_id || 0}-${setting_name}`);
let initialStorageSetting = localStorageSetting === null ? default_value : localStorageSetting;
let newSetting = GetMythicSetting({setting_name, default_value});
setSetting(newSetting);
}, [preferences?.[setting_name]]);
switch(output){
case "boolean":
initialStorageSetting = (initialStorageSetting.toLowerCase() === "true");
break;
case "number":
initialStorageSetting = Number(initialStorageSetting);
break;
default:
console.log("unknown output type", output);
}
setSetting(initialStorageSetting);
}, [me]);
return setting;
}
export function GetMythicSetting({setting_name, default_value}){
const preferences = mePreferences();
return preferences?.[setting_name] === undefined ? default_value : preferences?.[setting_name];
}
export function useSetMythicSetting() {
const [updateSetting] = useMutation(updatePreferences, {
onCompleted: (data) => {
//console.log(data, mePreferences());
},
onError: (error) => {
snackActions.error("failed to save user setting: " + error.message);
console.log(error);
}
});
return [
({setting_name, value}) => {
if(mePreferences()?.[setting_name] !== value){
const updatedPreferences = {
...mePreferences(),
[setting_name]: value,
};
mePreferences(updatedPreferences);
updateSetting({variables: {preferences: updatedPreferences}});
}
},
({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([]);
@@ -57,7 +58,7 @@ export function MythicSelectFromListDialog(props) {
<em>None</em>
</MenuItem>
{options.map( (opt) => (
<MenuItem value={opt} key={opt[props.identifier]}>{opt[props.display]}</MenuItem>
<MenuItem value={opt} key={opt[props.identifier]}>{opt?.[props.display]}</MenuItem>
) )}
</Select>
</FormControl>
@@ -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,25 +1,17 @@
import React from 'react';
import { Tooltip } from '@mui/material';
import {useTheme} from '@mui/material/styles';
export function MythicStyledTooltip(props){
const { children, title, style, ...other} = props;
export function MythicStyledTooltip({ children, title, enterDelay, tooltipStyle}){
const theme = useTheme();
return (
<Tooltip title={title} arrow followCursor enterDelay={1000} componentsProps={{
tooltip: {
sx: {
backgroundColor: theme.palette.background.contrast,
color: theme.palette.text.contrast,
boxShadow: theme.shadows[1],
fontSize: 13,
'& .MuiTooltip-arrow': {
color: theme.palette.background.contrast
}
}
}
}} style={{zIndex:1000}} {...other}>
{<span style={{...style, display: "inline-block"}}>{children}</span>}
</Tooltip>
);
<span style={{display: "inline-block", ...tooltipStyle}}
data-tooltip-id={"my-tooltip"}
data-tooltip-content={title}
data-tooltip-variant={theme.palette.mode === 'dark' ? 'light' : 'dark'}
data-tooltip-delay-show={enterDelay ? enterDelay : 750}
>
{children}
</span>
)
}
@@ -2,17 +2,19 @@ import { IconButton } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import Tab from '@mui/material/Tab';
import React from 'react';
import { useCallback } from 'react';
import Grow from '@mui/material/Grow';
import Popper from '@mui/material/Popper';
import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import Paper from '@mui/material/Paper';
import {useTheme} from '@mui/material/styles';
import Badge from '@mui/material/Badge';
import {
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
@@ -37,57 +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();
}
function drag(ev) {
ev.dataTransfer.setData("text", ev.target.id);
ev.dataTransfer.setData("opacity", ev.target.style.opacity);
ev.target.style.opacity = "0.2";
}
function onDragEnter(ev){
let node = ev.target.getAttribute("role") === "tab" ? ev.target : ev.target.closest('[role="tab"]');
node.style.borderLeft = "3px dotted red";
//console.log("ondragenter");
}
function onDragLeave(ev){
let node = ev.target.closest('[role="tab"]');
if(ev.target.getAttribute("role") === "tab"){
//console.log("leaving tab", ev.target)
node.style.border = "";
} else if(node.contains(ev.target)){
//console.log("onDragLeave, nodeContains", node, ev.target)
}else{
node.style.border = "";
//console.log("onDragLeave, not contains", node, ev.target)
}
}
function drop(ev) {
// shares dataTransfer with drag function
ev.preventDefault();
const data = ev.dataTransfer.getData("text");
const tabList = ev.target.closest("div[role='tablist']");
let node = ev.target.getAttribute("role") === "tab" ? ev.target : ev.target.closest('[role="tab"]');
//console.log(tabList, data, node.nextSibling);
node.style.border = "";
document.getElementById(data).style.opacity = ev.dataTransfer.getData("opacity");
for(let i = 0; i < tabList.children.length; i++){
tabList.children[i].style.border = "";
}
//tabList.insertBefore(document.getElementById(data), node.nextSibling);
//console.log("selected", data, "toLeftOf", node.id);
return {"selected": data, "toLeftOf": node.id};
}
export function MythicTabLabel(props) {
const {
label,
@@ -103,17 +60,21 @@ export function MythicTabLabel(props) {
onEditTabDescription,
getCallbackData,
onDragTab,
selectedIndex,
highlight,
newDataForTab,
...other
} = props;
const onClick = (e) => {
e.stopPropagation();
e.preventDefault();
onCloseTab({ tabID: tabInfo.tabID, index: index });
};
const theme = useTheme();
const [openContextMenu, setOpenContextMenu] = React.useState(false);
const dropdownAnchorRef = React.useRef(null);
const handleContextClick = (event) => {
event.preventDefault();
event.stopPropagation();
if(contextMenuOptions && contextMenuOptions.length > 0){
setOpenContextMenu(true);
}
@@ -129,51 +90,58 @@ export function MythicTabLabel(props) {
setOpenContextMenu(false);
};
return (
<Tab
onDrop={(ev) => onDragTab(drop(ev))}
onDragOver={allowDrop}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
draggable={!!onDragTab}
onDragStart={drag}
label={
<span onContextMenu={handleContextClick} style={{ display: 'inline-block', zIndex: 1}} ref={dropdownAnchorRef}>
{label}
<IconButton component='div' size='small' onClick={onClick} {...other}>
<CloseIcon />
</IconButton>
<Draggable draggableId={`callbacks-tab-${tabInfo.tabID}`} index={index} disableInteractiveElementBlocking={true}>
{(provided, snapshot) => (
<div ref={provided.innerRef}
{...provided.draggableProps}>
<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" style={{backgroundColor: theme.palette.mode === 'dark' ? theme.palette.primary.dark : theme.palette.primary.light, color: "white"}}>
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
<MenuList id="split-button-menu" >
{contextMenuOptions.map((option, index) => (
<MenuItem
key={option.name + index}
onClick={(event) => handleMenuItemClick(event, index)}
>
{option.name}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</span>
}
{...a11yProps(index)}
{...other}
style={{padding: "0px 5px 0px 5px"}}
/>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
}}
>
<Paper variant="outlined" className={"dropdownMenuColored"}>
<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;
@@ -5,7 +5,7 @@ import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import MythicTextField from './MythicTextField';
import {useQuery, gql, useMutation} from '@apollo/client';
import { Select, Input, MenuItem, Link, IconButton, } from '@mui/material';
import { Select, Input, MenuItem, Link, IconButton } from '@mui/material';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
@@ -24,14 +24,22 @@ import WebhookIcon from '@mui/icons-material/Webhook';
import Chip from '@mui/material/Chip';
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
}
}
`;
@@ -46,7 +54,7 @@ const updateTagMutationTemplate = gql`
const getObjectTagsQueryTemplate = ({target_object}) => {
return gql`
query getObjectTags ($${target_object}: Int!) {
tag(where: {${target_object}: {_eq: $${target_object}}}) {
tag(where: {${target_object}: {_eq: $${target_object}}}, order_by: {tagtype: {name: asc}}) {
source
url
id
@@ -63,7 +71,7 @@ query getObjectTags ($${target_object}: Int!) {
}
const getTagtypesQuery = gql`
query getTagtype {
tagtype {
tagtype(order_by: {name: asc}) {
name
color
description
@@ -85,6 +93,17 @@ query getSingleTag($tag_id: Int!){
url
id
data
apitokens_id
credential_id
filemeta_id
keylog_id
mythictree_id
operation_id
response_id
callback_id
payload_id
task_id
taskartifact_id
tagtype {
name
description
@@ -94,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();
@@ -117,9 +137,26 @@ 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"}
}}
/>
{openTagDisplay &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openTagDisplay}
onClose={onClose}
@@ -151,7 +188,7 @@ const StringTagDataEntry = ({name, value}) => {
console.log("There was an error!", error);
})
}
if(RegExp(regex).test(value)){
if(RegExp(regex)?.test(value)){
const capturePieces = RegExp(captureRegex).exec(value);
const targetPieces = RegExp(targetRegex).exec(capturePieces[3]);
const colorPieces = RegExp(colorRegex).exec(capturePieces[3]);
@@ -173,7 +210,7 @@ const StringTagDataEntry = ({name, value}) => {
return (
<Link href={capturePieces[2]} color="textPrimary" target={"_blank"} >{capturePieces[1]}</Link>
)
} else if(value.startsWith("http")){
} else if(value.startsWith("http:") || value.startsWith("https:")){
return (
<>
{"Click for: "}
@@ -186,22 +223,50 @@ const StringTagDataEntry = ({name, value}) => {
function ViewTagDialog(props) {
const theme = useTheme();
const [selectedTag, setSelectedTag] = React.useState({});
const [objectInfo, setObjectInfo] = React.useState({object_type: "", object_id: ""});
const {} = useQuery(getSingleTag, {
variables: {tag_id: props.target_object_id},
onCompleted: data => {
if(data.tag_by_pk.apitokens_id !== null){
setObjectInfo({object_type: "apitokens_id", object_id: data.tag_by_pk.apitokens_id});
}else if(data.tag_by_pk.credential_id !== null){
setObjectInfo({object_type: "credential_id", object_id: data.tag_by_pk.credential_id});
}else if(data.tag_by_pk.filemeta_id !== null){
setObjectInfo({object_type: "filemeta_id", object_id: data.tag_by_pk.filemeta_id});
}else if(data.tag_by_pk.keylog_id !== null){
setObjectInfo({object_type: "keylog_id", object_id: data.tag_by_pk.keylog_id});
}else if(data.tag_by_pk.mythictree_id !== null){
setObjectInfo({object_type: "mythictree_id", object_id: data.tag_by_pk.mythictree_id});
}else if(data.tag_by_pk.response_id !== null){
setObjectInfo({object_type: "response_id", object_id: data.tag_by_pk.response_id});
}else if(data.tag_by_pk.task_id !== null){
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;
try{
tagData = JSON.parse(newTag.data);
newTag.data = tagData;
newTag.is_json = true;
if(newTag.data.constructor === Object){
newTag.data = {...data.tag_by_pk.data};
newTag.is_json = true;
} else if(typeof newTag.data === "string"){
tagData = JSON.parse(newTag.data);
newTag.data = tagData;
newTag.is_json = true;
}
}catch(error){
newTag.is_json = false;
}
setSelectedTag(newTag);
},
onError: error => {
console.log(error);
console.log("query error", error);
},
fetchPolicy: "network-only"
})
@@ -212,17 +277,21 @@ 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>
<TableCell style={{width: "20%"}}>Tag Type</TableCell>
<TableCell style={{display: "inline-flex", flexDirection: "row", width: "100%"}}>
<Chip label={selectedTag?.tagtype?.name||""} size="small" style={{float: "right", backgroundColor:selectedTag?.tagtype?.color||""}} />
<ViewEditTags target_object={objectInfo.object_type} target_object_id={objectInfo.object_id} me={props.me} />
</TableCell>
</TableRow>
<TableRow hover>
@@ -230,38 +299,60 @@ 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>
<Link href={selectedTag?.url || "#"} color="textPrimary" target="_blank" referrerPolicy='no'>{selectedTag?.url ? "click here" : "No reference link provided"}</Link>
</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>
)}
</MythicStyledTableCell>
</TableRow>
<TableRow>
<TableCell>Data</TableCell>
<TableCell>
{selectedTag?.is_json || false ? (
{selectedTag?.is_json ? (
<TableContainer className="mythicElement">
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
<TableBody>
{Object.keys(selectedTag.data).map( key => (
<TableRow key={key} hover>
<TableCell>{key}</TableCell>
<MythicStyledTableCell>{key}</MythicStyledTableCell>
{typeof selectedTag.data[key] === "string" ? (
<TableCell>
<StringTagDataEntry name={key} value={selectedTag.data[key]} />
</TableCell>
<MythicStyledTableCell style={{whiteSpace: "pre-wrap"}}>
<StringTagDataEntry name={key} value={String(selectedTag.data[key])} />
</MythicStyledTableCell>
) : typeof selectedTag.data[key] === "object" ? (
<TableCell>{selectedTag.data[key].toString()}</TableCell>
Array.isArray(selectedTag.data[key]) ? (
<TableCell style={{whiteSpace: "pre-wrap"}}>{JSON.stringify(selectedTag.data[key], null, 2)}</TableCell>
) : (
<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>{selectedTag.data[key]}</TableCell>
<MythicStyledTableCell>{String(selectedTag.data[key])}</MythicStyledTableCell>
)
}
@@ -318,7 +409,16 @@ export function ViewEditTagsDialog(props) {
setSelectedTag(data.tag[0]);
setNewSource(data.tag[0].source);
setNewURL(data.tag[0].url);
setNewData(data.tag[0].data);
try{
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));
}
}
},
onError: error => {
@@ -333,7 +433,11 @@ export function ViewEditTagsDialog(props) {
setExistingTags(newTags);
if(newTags.length > 0){
setSelectedTag(newTags[0]);
setNewData(newTags[0].data);
try{
setNewData(JSON.stringify(newTags[0].data, null, 2));
}catch(error){
setNewData(String(newTags[0].data));
}
setNewSource(newTags[0].source);
setNewURL(newTags[0].url);
} else {
@@ -374,9 +478,13 @@ const onEditorChange = (value, event) => {
}
const handleTaskTypeChange = (evt) => {
setSelectedTag(evt.target.value);
setNewData(evt.target.value.data);
setNewSource(evt.target.value.source);
setNewURL(evt.target.value.url);
try{
setNewData(JSON.stringify(evt.target.value.data, null, 2));
}catch(error){
setNewData(String(evt.target.value.data));
}
}
const handleNewTagCreate = ({tagtype_id, source, url, data, id}) => {
props.onClose();
@@ -388,25 +496,26 @@ const onAcceptDelete = () => {
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Edit Tags
<Button variant='contained' color="success" style={{float: "right"}} onClick={() => setOpenNewDialog(true)} >New</Button>
</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</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 />
</IconButton>
</MythicStyledTooltip>
<Select
labelId="demo-dialog-select-label"
id="demo-dialog-select"
@@ -423,32 +532,31 @@ return (
{selectedTag.id &&
<IconButton size="small" style={{float: "right"}} onClick={()=>{setOpenDeleteDialog(true);}} color="error" variant="contained"><DeleteIcon/></IconButton>
}
{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"}
@@ -464,11 +572,10 @@ return (
tabSize: 4,
useWorker: false
}}/>
</TableCell>
</MythicStyledTableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
@@ -502,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);
@@ -532,14 +644,25 @@ export function NewTagDialog(props) {
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Create new Tag Instance</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: "30%"}}>Select Existing Tag Type</TableCell>
<TableCell>
<MythicStyledTableCell style={{width: "20%"}}>
<Typography>
Tag
</Typography>
<Typography size="small" component="span" style={{fontSize: theme.typography.pxToRem(15)}}>
To create a new tag type click <Link style={{wordBreak: "break-all"}}
color="textPrimary"
href={"/new/tagtypes"}
underline="always" target="_blank">
here
</Link>
</Typography>
</MythicStyledTableCell>
<MythicStyledTableCell>
<Select
labelId="demo-dialog-select-label"
id="demo-dialog-select"
@@ -553,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"}
@@ -586,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">
@@ -606,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){
@@ -617,12 +740,15 @@ 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>
{openTagDialog ?
(<MythicDialog fullWidth={true} maxWidth="xl" open={openTagDialog}
<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)}}
innerDialog={<ViewEditTagsDialog me={me} target_object={target_object} target_object_id={target_object_id} onClose={(e)=>{toggleTagDialog(e, false)}} />}
/>) : null}
/>}
</React.Fragment>
)
@@ -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,108 +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,
}
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}
onKeyDown={this.onKeyPress}
label={this.props.showLabel === undefined ? this.props.name : this.props.showLabel ? this.props.name : undefined}
autoFocus={this.props.autoFocus}
variant={this.props.variant === undefined ? "outlined" : this.props.variant}
data-lpignore={true}
autoComplete={this.props.autoComplete === undefined ? "off" : (this.props.autoComplete ? "on" : "off")}
disabled={this.props.disabled === undefined ? false : this.props.disabled}
required={this.props.requiredValue ? this.props.requiredValue : false}
InputLabelProps={this.props.inputLabelProps}
multiline={this.props.multiline ? this.props.multiline : false}
maxRows={this.props.maxRows}
error={this.checkError()}
type={this.props.type === undefined ? "text" : this.props.type}
onWheel={ event => event.target.blur() }
InputProps={this.props.InputProps}
helperText={this.checkError() ? this.props.errorText : this.props.helperText}
style={{
padding:0,
marginBottom: this.props.marginBottom ? this.props.marginBottom : "10px",
display: this.props.inline ? "inline-block": "",
}}
classes={{
root: classes.root
}} />
</Root>
);
}
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;
-567
View File
@@ -1,567 +0,0 @@
import React from 'react';
import { styled } from '@mui/material/styles';
import clsx from 'clsx';
import { useTheme } from '@mui/material/styles';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import IconButton from '@mui/material/IconButton';
import MenuIcon from '@mui/icons-material/Menu';
import MenuItem from '@mui/material/MenuItem';
import ManageAccountsIcon from '@mui/icons-material/ManageAccounts';
import Menu from '@mui/material/Menu';
import { Link } from 'react-router-dom';
import Drawer from '@mui/material/Drawer';
import List from '@mui/material/List';
import Divider from '@mui/material/Divider';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import ListItem from '@mui/material/ListItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import { useReactiveVar } from '@apollo/client';
import { menuOpen, FailedRefresh } from '../cache';
import { TopAppBarNotifications } from './TopAppBarNotifications';
import { EventFeedNotifications } from './EventFeedNotifications';
import HelpIcon from '@mui/icons-material/Help';
import PhoneCallbackIcon from '@mui/icons-material/PhoneCallback';
import {ReactComponent as ReactLogo} from './mythic_red_small.svg';
import SpaceDashboardTwoToneIcon from '@mui/icons-material/SpaceDashboardTwoTone';
import ListSubheader from '@mui/material/ListSubheader';
import Collapse from '@mui/material/Collapse';
import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore';
import LayersTwoToneIcon from '@mui/icons-material/LayersTwoTone';
import TableChartTwoToneIcon from '@mui/icons-material/TableChartTwoTone';
import PostAddIcon from '@mui/icons-material/PostAdd';
import SupervisorAccountIcon from '@mui/icons-material/SupervisorAccount';
import EditIcon from '@mui/icons-material/Edit';
import { Typography } from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import BarChartIcon from '@mui/icons-material/BarChart';
import HeadsetTwoToneIcon from '@mui/icons-material/HeadsetTwoTone';
import CodeIcon from '@mui/icons-material/Code';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faBiohazard, faFlagCheckered} from '@fortawesome/free-solid-svg-icons';
import AttachmentIcon from '@mui/icons-material/Attachment';
import FingerprintIcon from '@mui/icons-material/Fingerprint';
import {faSocks} from '@fortawesome/free-solid-svg-icons';
import CameraAltTwoToneIcon from '@mui/icons-material/CameraAltTwoTone';
import {mythicVersion, mythicUIVersion} from '../index';
import {MythicStyledTooltip} from './MythicComponents/MythicStyledTooltip';
import VpnKeyIcon from '@mui/icons-material/VpnKey';
import ThumbDownTwoTone from '@mui/icons-material/ThumbDownTwoTone';
import { MythicDialog } from './MythicComponents/MythicDialog';
import {MythicFeedbackDialog} from './MythicComponents/MythicFeedbackDialog';
import LocalOfferTwoToneIcon from '@mui/icons-material/LocalOfferTwoTone';
import StorageIcon from '@mui/icons-material/Storage';
import PublicIcon from '@mui/icons-material/Public';
import LightModeTwoToneIcon from '@mui/icons-material/LightModeTwoTone';
import DarkModeTwoToneIcon from '@mui/icons-material/DarkModeTwoTone';
const PREFIX = 'TopAppBar';
const classes = {
root: `${PREFIX}-root`,
title: `${PREFIX}-title`,
hide: `${PREFIX}-hide`,
drawer: `${PREFIX}-drawer`,
drawerPaper: `${PREFIX}-drawerPaper`,
drawerHeader: `${PREFIX}-drawerHeader`,
listSubHeader: `${PREFIX}-listSubHeader`,
appBar: `${PREFIX}-appBar`,
appBarShift: `${PREFIX}-appBarShift`,
nested: `${PREFIX}-nested`,
mythicElement: `${PREFIX}-mythicElement`
};
const StyledAppBar = styled(AppBar)((
{
theme
}
) => ({
[`& .${classes.hide}`]: {
display: 'none',
},
[`&.${classes.appBar}`]: {
width: "100%",
backgroundColor: theme.topAppBarColor,
transition: theme.transitions.create(['margin', 'width'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
},
[`&.${classes.appBarShift}`]: {
maxWidth: `calc(100% - ${drawerWidth}px)`,
marginLeft: drawerWidth,
transition: theme.transitions.create(['margin', 'width'], {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
},
}));
const StyledDrawer = styled(Drawer)((
{
theme
}
) => ({
[`& .${classes.title}`]: {
flexGrow: 1,
},
[`&.${classes.drawer}`]: {
width: drawerWidth,
flexShrink: 0,
},
[`& .${classes.drawerPaper}`]: {
width: drawerWidth,
},
[`& .${classes.drawerHeader}`]: {
display: 'flex',
alignItems: 'center',
padding: theme.spacing(0, 1),
justifyContent: 'flex-end',
},
[`& .${classes.listSubHeader}`]: {
backgroundColor: theme.palette.listSubHeader.default
},
[`& .${classes.nested}`]: {
paddingLeft: theme.spacing(4),
},
}));
const drawerWidth = 240;
export function TopAppBar(props) {
const theme = useTheme();
const feedbackRef = React.useRef(null);
const settingsRef = React.useRef(null);
const documentationRef = React.useRef(null);
const [anchorEl, setAnchorEl] = React.useState(null);
const [documentationAnchorEl, setDocumentationAnchorEl] = React.useState(null);
const me = props.me;
const isOpen = useReactiveVar(menuOpen);
const [openGlobal, setOpenGlobal] = React.useState(false);
const [openCreate, setOpenCreate] = React.useState(false);
const [openOperations, setOpenOperations] = React.useState(false);
const [openData, setOpenData] = React.useState(false);
const [openFeedbackForm, setOpenFeedbackForm] = React.useState(false);
const handleDrawerOpen = () => {
menuOpen(true);
};
const handleDrawerClose = () => {
menuOpen(false);
};
const handleMenu = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = (evt) => {
setAnchorEl(null);
};
const handleDocumentationMenu = (event) => {
setDocumentationAnchorEl(event.currentTarget);
};
const handleDocumentationClose = (evt) => {
setDocumentationAnchorEl(null);
};
const handleToggleGlobal = () => {
setOpenGlobal(!openGlobal);
}
const handleToggleCreate = () => {
setOpenCreate(!openCreate);
}
const handleToggleOperations = () => {
setOpenOperations(!openOperations);
}
const handleToggleData = () => {
setOpenData(!openData);
}
const handleLogout = () => {
menuOpen(false);
console.log("clicked logout, calling FailedRefresh");
FailedRefresh();
}
return (
<>
{me?.user?.current_operation_id ? (<EventFeedNotifications me={me} />) : null }
<StyledAppBar className={clsx(classes.appBar, {[classes.appBarShift]: isOpen,})}>
{ me?.loggedIn ? (
<Toolbar variant="dense" >
<IconButton
edge="start"
className={clsx(isOpen && classes.hide)}
color="inherit"
aria-label="menu"
disableFocusRipple={true}
disableRipple={true}
onClick={handleDrawerOpen}
>
<MenuIcon fontSize={"large"} />
</IconButton>
<div style={{width: "100%"}}>
<MythicStyledTooltip title="C2 Profiles and Payload Types">
<IconButton component={Link} to='/new/payloadtypes' color="inherit" size="large" disableFocusRipple={true}
disableRipple={true}>
<HeadsetTwoToneIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Payloads">
<IconButton component={Link} to='/new/payloads' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<FontAwesomeIcon size={"lg"} icon={faBiohazard} />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Search Operation">
<IconButton component={Link} to='/new/search?tab=callbacks&searchField=Host&search=' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<SearchIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Files">
<IconButton
component={Link}
to='/new/search?searchField=Filename&tab=files&location=Downloads'
color="inherit"
disableFocusRipple={true}
disableRipple={true}>
<AttachmentIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Artifacts">
<IconButton
disableFocusRipple={true}
disableRipple={true}
component={Link}
to='/new/search?searchField=Artifact&tab=artifacts'
color="inherit">
<FingerprintIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Proxies">
<IconButton component={Link} to='/new/search?tab=socks' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<FontAwesomeIcon size={"lg"} icon={faSocks} />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Screenshots" arrow >
<IconButton
component={Link}
to='/new/search?searchField=Filename&tab=files&location=Screenshots'
color="inherit"
disableFocusRipple={true}
disableRipple={true}
>
<CameraAltTwoToneIcon fontSize={"large"} />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Credentials" arrow >
<IconButton
component={Link}
to='/new/search?searchField=Account&tab=credentials'
color="inherit"
disableFocusRipple={true}
disableRipple={true}>
<VpnKeyIcon fontSize={"large"} className="mythicElement" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Active Callbacks">
<IconButton component={Link} to='/new/callbacks' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<PhoneCallbackIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Reporting" >
<IconButton component={Link} to='/new/reporting' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<FontAwesomeIcon size={"lg"} icon={faFlagCheckered} />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="MITRE ATT&CK" >
<IconButton component={Link} to='/new/mitre' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<TableChartTwoToneIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Operation Tags" >
<IconButton component={Link} to='/new/tagtypes' color="inherit" disableFocusRipple={true}
disableRipple={true}>
<LocalOfferTwoToneIcon fontSize={"large"} className="mythicElement"/>
</IconButton>
</MythicStyledTooltip>
<Link style={{display: "inline-flex", alignItems: "center", paddingRight: "10px", color: "white", textDecoration: "none"}} to="/new/operations">
{me?.user?.current_operation || "No Operation Set"}
</Link>
<Menu
id="menu-appbar"
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
open={Boolean(anchorEl)}
onClose={handleClose}
MenuListProps={{style: {backgroundColor: theme.palette.primary.main, color: "white"}}}
>
<MenuItem divider={true} style={{display: "block"}} component={Link} to="/new/settings" onClick={handleClose} name="settings">
<Typography paragraph={true} variant="caption" style={{marginBottom: "0", color: "white"}}>Signed in as:</Typography>
<Typography paragraph={true} variant="body1" style={{marginBottom: "0", fontWeight: 600, color: "white"}}> {me?.user?.username || "" } </Typography>
</MenuItem>
<MenuItem component={Link} to="/new/login" onClick={handleLogout}>Logout</MenuItem>
</Menu>
<div style={{display: "inline-flex", justifyContent: "flex-end", float: "right"}}>
<IconButton
aria-label="feedback"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={() => setOpenFeedbackForm(true)}
ref={feedbackRef}
color="inherit"
style={{float: "right", }}
disableFocusRipple={true}
disableRipple={true}>
<MythicStyledTooltip title="Send Support Feedback">
<ThumbDownTwoTone fontSize={"large"} className="mythicElement" />
</MythicStyledTooltip>
</IconButton>
{openFeedbackForm &&
<MythicDialog fullWidth={true} maxWidth="md" open={openFeedbackForm}
onClose={()=>{setOpenFeedbackForm(false);}}
innerDialog={<MythicFeedbackDialog
title={"Submit Feedback via Webhook"}
onClose={()=>{setOpenFeedbackForm(false);}} />}
/>
}
<IconButton
aria-label="documentation links"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={handleDocumentationMenu}
ref={documentationRef}
color="inherit"
style={{float:"right"}}
disableFocusRipple={true}
disableRipple={true}>
<MythicStyledTooltip title="Help">
<HelpIcon fontSize={"large"} className="mythicElement"/>
</MythicStyledTooltip>
</IconButton>
<Menu
id="menu-appbar"
anchorEl={documentationAnchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
open={Boolean(documentationAnchorEl)}
onClose={handleDocumentationClose}
MenuListProps={{style: {backgroundColor: theme.palette.primary.main, color: "white"}}}
>
<MenuItem component={Link} target="_blank" to="/docs/agents" onClick={handleDocumentationClose}>Agent Documentation</MenuItem>
<MenuItem component={Link} target="_blank" to="/docs/wrappers" onClick={handleDocumentationClose}>Wrapper Documentation</MenuItem>
<MenuItem component={Link} target="_blank" to="/docs/c2-profiles" onClick={handleDocumentationClose}>C2 Profile Documentation</MenuItem>
<MenuItem component={Link} to={{pathname: "https://docs.mythic-c2.net"}} target="_blank" onClick={handleDocumentationClose}>Mythic Documentation</MenuItem>
</Menu>
<TopAppBarNotifications />
<IconButton
aria-label="account of current user"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={handleMenu}
ref={settingsRef}
color="inherit"
style={{float: "right", }}
disableFocusRipple={true}
disableRipple={true} >
<MythicStyledTooltip title="Settings or Logout">
<ManageAccountsIcon fontSize={"large"} className="mythicElement" />
</MythicStyledTooltip>
</IconButton>
{props.theme === 'dark' &&
<IconButton
disableFocusRipple={true}
disableRipple={true}
onClick={props.toggleTheme}
style={{float: "right", paddingRight: "0px", color: '#2f0e67'}}
>
<MythicStyledTooltip title="Change to Light Mode">
<DarkModeTwoToneIcon fontSize={"large"} className="mythicElement" />
</MythicStyledTooltip>
</IconButton>
}
{props.theme === 'light' &&
<IconButton
disableFocusRipple={true}
disableRipple={true}
onClick={props.toggleTheme}
style={{float: "right", paddingRight: "0px", color: '#eacc1b'}}
>
<MythicStyledTooltip title="Change to Dark Mode">
<LightModeTwoToneIcon fontSize={"large"} className="mythicElement" />
</MythicStyledTooltip>
</IconButton>
}
</div>
</div>
</Toolbar>
) : null
}
</StyledAppBar>
<StyledDrawer
className={classes.drawer}
anchor="left"
open={isOpen}
classes={{
paper: classes.drawerPaper,
}}
onClose={handleDrawerClose}
>
<div className={classes.drawerHeader} role="presentation">
<ReactLogo style={{width: "80%", display: "inline-block"}}/>
<IconButton onClick={handleDrawerClose} size="large">
{theme.direction === 'ltr' ? <ChevronLeftIcon className="mythicElement"/> : <ChevronRightIcon className="mythicElement"/>}
</IconButton>
</div>
<Divider />
<List
subheader={
<ListSubheader className={classes.listSubHeader} component="div" id="nested-list-subheader">
Home
</ListSubheader>
}>
<div style={{marginLeft: "15px"}}>
<b>Mythic Version:</b> v{mythicVersion}<br/>
<b>UI Version:</b> v{mythicUIVersion}<br/>
</div>
<ListItem button component={Link} to='/new' key={"home"} onClick={handleDrawerClose}>
<ListItemIcon ><SpaceDashboardTwoToneIcon fontSize={"large"} className="mythicElement" /></ListItemIcon>
<ListItemText primary={"Dashboard"} />
</ListItem>
</List>
<Divider />
<List
subheader={
<ListSubheader className={classes.listSubHeader} component="div" id="nested-list-subheader">
Global Configurations
</ListSubheader>
}>
<ListItem button onClick={handleToggleGlobal}>
<ListItemIcon><LayersTwoToneIcon fontSize={"large"} /></ListItemIcon>
<ListItemText>Services</ListItemText>
{openGlobal ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={openGlobal} unmountOnExit>
<List component="div" disablePadding>
<ListItem button className={classes.nested} target="_blank" component={Link} to='/jupyter' key={"jupyter"} onClick={handleDrawerClose}>
<ListItemIcon><CodeIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Jupyter Notebooks"} />
</ListItem>
<ListItem button className={classes.nested} target="_blank" component={Link} to='/console' key={"console"} onClick={handleDrawerClose}>
<ListItemIcon><StorageIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"GraphQL Console"} />
</ListItem>
<ListItem button className={classes.nested} component={Link} to='/new/consuming_services' key={"consuming"} onClick={handleDrawerClose}>
<ListItemIcon><PublicIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Consuming Services"} />
</ListItem>
</List>
</Collapse>
<ListItem button onClick={handleToggleCreate}>
<ListItemIcon><PostAddIcon /></ListItemIcon>
<ListItemText>Create</ListItemText>
{openCreate ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={openCreate} unmountOnExit>
<List component="div" disablePadding>
<ListItem button className={classes.nested} component={Link} to='/new/createpayload' key={"createpayload"} onClick={handleDrawerClose} state={{from: 'TopAppBar'}}>
<ListItemIcon><PostAddIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Create Payload"} />
</ListItem>
<ListItem button className={classes.nested} component={Link} to='/new/createwrapper' key={"createwrapper"} onClick={handleDrawerClose}>
<ListItemIcon><PostAddIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Create Wrapper"} />
</ListItem>
</List>
</Collapse>
<ListItem button onClick={handleToggleOperations}>
<ListItemIcon><SupervisorAccountIcon fontSize={"large"} /></ListItemIcon>
<ListItemText>Operation Config</ListItemText>
{openOperations ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={openOperations} unmountOnExit>
<List component="div" disablePadding>
<ListItem button className={classes.nested} component={Link} to='/new/payloadtypes' key={"payloadtypes"} onClick={handleDrawerClose}>
<ListItemIcon><HeadsetTwoToneIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Agents & C2"} />
</ListItem>
<ListItem button className={classes.nested} component={Link} to='/new/operations' key={"modifyoperations"} onClick={handleDrawerClose}>
<ListItemIcon><EditIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Modify Operations"} />
</ListItem>
<ListItem button className={classes.nested} component={Link} to='/new/browserscripts' key={"browserscripts"} onClick={handleDrawerClose}>
<ListItemIcon><CodeIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"BrowserScripts"} />
</ListItem>
</List>
</Collapse>
</List>
<Divider />
<List
subheader={
<ListSubheader className={classes.listSubHeader} component="div" id="nested-list-subheader">
Operational Views
</ListSubheader>
}>
<ListItem button onClick={handleToggleData}>
<ListItemIcon><BarChartIcon fontSize={"large"} /></ListItemIcon>
<ListItemText>Operational Data</ListItemText>
{openData ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={openData} unmountOnExit>
<List component="div" disablePadding>
<ListItem button className={classes.nested} component={Link} to='/new/payloads' key={"payloads"} onClick={handleDrawerClose}>
<ListItemIcon><FontAwesomeIcon icon={faBiohazard} size="2x"/></ListItemIcon>
<ListItemText primary={"Payloads"} />
</ListItem>
<ListItem button className={classes.nested} component={Link} to='/new/search?tab=callbacks&searchField=Host&search=' key={"search"} onClick={handleDrawerClose}>
<ListItemIcon><SearchIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Search"} />
</ListItem>
</List>
</Collapse>
<ListItem button component={Link} to='/new/callbacks' key={"callbacks"} onClick={handleDrawerClose}>
<ListItemIcon><PhoneCallbackIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
<ListItemText primary={"Active Callbacks"} />
</ListItem>
</List>
<Divider />
</StyledDrawer>
</>
);
}
@@ -0,0 +1,49 @@
import React from 'react';
import {useSubscription, gql } from '@apollo/client';
import Badge from '@mui/material/Badge';
import NotificationsActiveTwoToneIcon from '@mui/icons-material/NotificationsActiveTwoTone';
import { useTheme } from '@mui/material/styles';
import {alertCount} from "../cache";
const SUB_Event_Logs = gql`
subscription OperationAlertCounts{
operation_stream(cursor: {initial_value: {updated_at: "1970-01-01"}, ordering: ASC}, batch_size: 1) {
id
alert_count
}
}
`;
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 (
error ? (
<Badge color="secondary" badgeContent={"X"}>
<NotificationsActiveTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} />
</Badge>
) : (
<Badge badgeContent={alerts}
color="error" max={99}
>
<NotificationsActiveTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"}/>
</Badge>
)
);
}
@@ -1,54 +0,0 @@
import React from 'react';
import {useSubscription, gql } from '@apollo/client';
import Badge from '@mui/material/Badge';
import NotificationsActiveTwoToneIcon from '@mui/icons-material/NotificationsActiveTwoTone';
import { Link } from 'react-router-dom';
import { IconButton } from '@mui/material';
import {snackActions} from './utilities/Snackbar';
import { meState } from '../cache';
import { useReactiveVar } from '@apollo/client';
import {MythicStyledTooltip} from "./MythicComponents/MythicStyledTooltip";
const SUB_Event_Logs = gql`
subscription MySubscription($operation_id: Int!) {
operation_by_pk(id: $operation_id) {
id
alert_count
}
}
`;
export function TopAppBarNotifications(props) {
const me = useReactiveVar(meState);
const { loading, error, data } = useSubscription(SUB_Event_Logs, {
variables: {operation_id: me?.user?.current_operation_id || 0},
onError: data => {
snackActions.error("Mythic encountered an error getting event log messages: " + data.toString());
console.error(data);
}
});
return (
<MythicStyledTooltip title="Event Feed">
<IconButton
color="inherit"
component={Link}
disableFocusRipple={true}
disableRipple={true}
to='/new/EventFeed'
style={{float: "right"}}>
{error ? (
<Badge color="secondary" badgeContent={0}>
<NotificationsActiveTwoToneIcon fontSize={"large"} />
</Badge>
) : (
<Badge badgeContent={data?.operation_by_pk?.alert_count || 0} color="error">
<NotificationsActiveTwoToneIcon fontSize={"large"} />
</Badge>
)}
</IconButton>
</MythicStyledTooltip>
);
}
File diff suppressed because it is too large. Load diff
@@ -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`
@@ -78,7 +79,7 @@ export function BrowserScripts({me}){
}
} )
setBrowserScripts(scripts);
if(backdropOpen){setBackdropOpen(false);}
setBackdropOpen(false);
}
});
const [toggleActive] = useMutation(updateBrowserScriptActive, {
@@ -117,6 +118,7 @@ export function BrowserScripts({me}){
}
});
const onToggleActive = ({browserscript_id, active}) => {
setBackdropOpen(true);
toggleActive({variables: {browserscript_id, active}});
}
const onSubmitEdit = ({browserscript_id, script, command_id, payload_type_id}) => {
@@ -134,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>
@@ -144,6 +146,6 @@ export function BrowserScripts({me}){
onSubmitEdit={onSubmitEdit} onRevert={onRevert} onSubmitNew={onSubmitCreateNewBrowserScript}
/>
</React.Fragment>
</MythicPageBody>
);
}
@@ -9,33 +9,33 @@ 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", marginTop: "10px", marginRight: "5px"}} variant={"elevation"}>
<Typography variant="h3" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
Browser Scripts
</Typography>
<Button size="small" onClick={() => setOpenNewScriptDialog(true)} style={{float: "right", marginTop: "10px", marginRight: "30px"}} startIcon={<AddCircleOutlineOutlinedIcon/>} color="primary" variant="contained">New Script</Button>
{openNewScriptDialog ? (
<>
<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 component={Paper} className="mythicElement" style={{maxHeight: "90%"}}>
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "100%", "overflow": "scroll"}}>
</MythicPageHeader>
<TableContainer className="mythicElement" style={{height: "100%", flexGrow: 1}}>
<Table stickyHeader={true} size="small" style={{"tableLayout": "fixed", "maxWidth": "100%", "overflow": "scroll"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "3rem"}}>Edit</TableCell>
@@ -58,8 +58,8 @@ export function BrowserScriptsTable(props){
))}
</TableBody>
</Table>
</TableContainer>
</React.Fragment>
</TableContainer>
</>
)
}
@@ -7,6 +7,7 @@ import {EditScriptDialog} from './EditScriptDialog';
import SettingsIcon from '@mui/icons-material/Settings';
import IconButton from '@mui/material/IconButton';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
export function BrowserScriptsTableRow(props){
const [openEdit, setOpenEdit] = React.useState(false);
@@ -29,17 +30,14 @@ export function BrowserScriptsTableRow(props){
<Switch
checked={props.active}
onChange={onToggleActive}
color="primary"
color="info"
inputProps={{ 'aria-label': 'checkbox', "track": "white" }}
name="Active"
/>
</TableCell>
<TableCell>
<MythicStyledTooltip title={props.payloadtype.name}>
<img
style={{width: "35px", height: "35px"}}
src={"/static/" + props.payloadtype.name + ".svg"}
/>
<MythicAgentSVGIcon payload_type={props.payloadtype.name} style={{width: "35px", height: "35px"}} />
</MythicStyledTooltip>
</TableCell>
<TableCell>{props.command.cmd}</TableCell>
@@ -111,7 +111,6 @@ export function EditScriptDialog(props) {
//setScript(atob(props.script));
setScript(decodeURIComponent(window.atob(props.script)));
}catch(error){
console.log(error)
setScript(props.script);
}
}
@@ -270,7 +269,7 @@ export function EditScriptDialog(props) {
input={<Input style={{width: "100%"}}/>}
>
{availableTasks.map( (opt) => (
<MenuItem value={opt} key={"task" + opt.id}>{opt.command_name + " " + opt.display_params}</MenuItem>
<MenuItem value={opt} key={"task" + opt.id}>{opt.command_name + " / " + opt.display_id + " / " + opt.display_params}</MenuItem>
) )}
</Select>
</FormControl>
@@ -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!) {
@@ -19,85 +20,130 @@ query GetCallbackDetails($callback_id: Int!) {
loadedcommands {
command {
cmd
payloadtype {
name
}
id
}
id
}
payload {
payloadtype {
commands {
name
commands(where: {deleted: {_eq: false}}) {
cmd
id
}
}
}
}
command(where: {deleted: {_eq: false}, payloadtype: {agent_type: {_eq: "command_augment"}}}){
cmd
id
payloadtype {
name
}
}
}
`;
const CustomListElement = React.memo(({valueObj, handleToggle}) => {
return (
<ListItem style={{padding: 0}} key={valueObj.display_value} role="listitem" button
onClick={() => handleToggle(valueObj)}>
<ListItemIcon>
<Checkbox
checked={valueObj.checked}
disableRipple
inputProps={{'aria-labelledby': valueObj.label}}
/>
</ListItemIcon>
<ListItemText id={valueObj.label} primary={valueObj.display_value}/>
</ListItem>
)
});
const CustomList = ({title, items, handleToggle}) => {
const renderedList = React.useMemo( () => {
return items.map((valueObj) => (
<CustomListElement key={valueObj.display_value} valueObj={valueObj} handleToggle={handleToggle} />
))
}, [items, handleToggle]);
return (
<>
<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 leftTitle = "Commands Not Included";
const rightTitle = "Commands Included";
useQuery(getCommandsQuery, {variables: {callback_id: props.callback_id},
fetchPolicy: "no-cache",
onCompleted: (data) => {
setOriginalLeft(data.callback_by_pk.payload.payloadtype.commands);
let originalLeftFromQuery = data.callback_by_pk.payload.payloadtype.commands.map(c => {
return {...c, payloadtype: {name: data.callback_by_pk.payload.payloadtype.name}}
})
originalLeftFromQuery = [...originalLeftFromQuery, ...data.command];
setOriginalRight(data.callback_by_pk.loadedcommands);
const leftData = data.callback_by_pk.payload.payloadtype.commands.reduce( (prev, cur) => {
if( data.callback_by_pk.loadedcommands.filter(c => c.command.cmd === cur.cmd).length === 0){
let leftData = originalLeftFromQuery.reduce( (prev, cur) => {
if( data.callback_by_pk.loadedcommands.filter(c => c.command.cmd === cur.cmd && c.command.payloadtype.name === cur.payloadtype.name).length === 0){
return [...prev, cur];
} else {
return [...prev];
}
}, []);
leftData.sort( (a,b) => a.cmd < b.cmd ? -1 : 1);
leftData = leftData.map( c => {return {...c,
checked: false,
display_value: c.cmd + " (" + c?.payloadtype?.name + ")",
label: `transfer-list-item-${c.cmd + " (" + c?.payloadtype?.name + ")"}-label`
}});
setLeft(leftData);
const rightData = data.callback_by_pk.loadedcommands.map( c => c.command);
let rightData = data.callback_by_pk.loadedcommands.map( c => c.command);
rightData.sort( (a,b) => a.cmd < b.cmd ? -1 : 1);
rightData = rightData.map( c => {return {...c,
checked: false,
display_value: c.cmd + " (" + c?.payloadtype?.name + ")",
label: `transfer-list-item-${c.cmd + " (" + c?.payloadtype?.name + ")"}-label`
}});
setRight(rightData);
},
onError: (data) => {
}
})
function not(a, b) {
if(props.itemKey){
return a.filter( (value) => b.find( (element) => element[props.itemKey] === value[props.itemKey] ) === undefined)
}
return a.filter((value) => b.indexOf(value) === -1);
}
function intersection(a, b) {
if(props.itemKey){
return a.filter( (value) => b.find( (element) => element[props.itemKey] === value[props.itemKey] ) !== undefined)
}
return a.filter((value) => b.indexOf(value) !== -1);
}
const handleToggle = (value) => () => {
let currentIndex = -1;
if(props.itemKey){
currentIndex = checked.findIndex( (element) => element[props.itemKey] === value[props.itemKey]);
}else{
currentIndex = checked.indexOf(value);
}
const newChecked = [...checked];
if (currentIndex === -1) {
newChecked.push(value);
} else {
newChecked.splice(currentIndex, 1);
const handleToggle = (value) => {
let found = false;
const newLeft = left.map( c => {
if(c.display_value === value.display_value){
c.checked = !c.checked;
found = true;
}
return c;
});
if(found){
setLeft(newLeft);
return
}
setChecked(newChecked);
const newRight = right.map( c => {
if(c.display_value === value.display_value){
c.checked = !c.checked;
}
return c;
});
setRight(newRight);
};
const handleAllRight = () => {
@@ -106,70 +152,47 @@ export function AddRemoveCallbackCommandsDialog(props) {
};
const handleCheckedRight = () => {
setRight(right.concat(leftChecked));
setLeft(not(left, leftChecked));
setChecked(not(checked, leftChecked));
let leftChecked = left.filter(c => c.checked);
leftChecked = leftChecked.map(c => {return {...c, checked: false}})
let leftNotChecked = left.filter(c => !c.checked);
setRight(right.concat(leftChecked));
setLeft(leftNotChecked);
};
const handleCheckedLeft = () => {
setLeft(left.concat(rightChecked));
setRight(not(right, rightChecked));
setChecked(not(checked, rightChecked));
let rightChecked = right.filter(c => c.checked);
rightChecked = rightChecked.map(c => {return {...c, checked: false}})
let rightNotChecked = right.filter(c => !c.checked);
setLeft(left.concat(rightChecked));
setRight(rightNotChecked);
};
const handleAllLeft = () => {
setLeft(left.concat(right));
setRight([]);
};
const customList = (title, items) => (
<>
<CardHeader className={classes.cardHeader} title={title} />
<StyledDivider classes={{root: classes.divider}}/>
<CardContent style={{flexGrow: 1, overflowY: "auto", padding: 0}}>
<List dense component="div" role="list" style={{padding:0, width: "100%"}}>
{items.map((valueObj) => {
const value = valueObj.cmd;
const labelId = `transfer-list-item-${value}-label`;
return (
<ListItem style={{padding:0}} key={value} role="listitem" button onClick={handleToggle(valueObj)}>
<ListItemIcon>
<Checkbox
checked={checked.findIndex( (element) => element.cmd === value) !== -1}
tabIndex={-1}
disableRipple
inputProps={{ 'aria-labelledby': labelId }}
/>
</ListItemIcon>
<ListItemText id={labelId} primary={value} />
</ListItem>
);
})}
<ListItem />
</List>
</CardContent>
</>
);
const setFinalTags = () => {
// things to add are in the `right` now but weren't for `originalRight`
const commandsToAdd = right.filter( (command) => {
return originalRight.filter(orig => orig.command.cmd == command.cmd).length == 0;
return originalRight.filter(orig => orig.command.cmd === command.cmd && orig.command.payloadtype.name === command.payloadtype.name).length === 0;
});
const commandsToRemove = originalRight.filter( (command) => {
return right.filter(newCommand => newCommand.cmd == command.command.cmd).length == 0;
return right.filter(newCommand => newCommand.cmd === command.command.cmd && newCommand.payloadtype.name === command.command.payloadtype.name).length === 0;
})
props.onSubmit({commandsToAdd, commandsToRemove});
props.onClose();
}
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Add or Remove Commands for Callback {props.display_id} </DialogTitle>
<DialogContent dividers={true} style={{height: "100%", display: "flex", flexDirection: "column", position: "relative", maxHeight: "100%"}}>
<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"
@@ -186,7 +209,6 @@ export function AddRemoveCallbackCommandsDialog(props) {
size="small"
className={classes.button}
onClick={handleCheckedRight}
disabled={leftChecked.length === 0}
aria-label="move selected right"
>
&gt;
@@ -196,7 +218,6 @@ export function AddRemoveCallbackCommandsDialog(props) {
size="small"
className={classes.button}
onClick={handleCheckedLeft}
disabled={rightChecked.length === 0}
aria-label="move selected left"
>
&lt;
@@ -213,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>
File diff suppressed because it is too large. Load diff
@@ -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,11 +106,23 @@ export const taskingDataFragment = gql`
domain
ip
mythictree_groups
pid
}
callback_id
commentOperator{
username
}
eventstepinstance {
eventgroupinstance {
eventgroup{
id
}
id
}
eventstep {
name
}
}
completed
id
display_id
@@ -111,20 +133,37 @@ export const taskingDataFragment = gql`
display_params
status
timestamp
status_timestamp_submitted
status_timestamp_processing
status_timestamp_preprocessing
command {
cmd
supported_ui_features
id
payloadtype {
name
}
}
command_name
opsec_pre_blocked
opsec_pre_bypassed
opsec_pre_bypass_user {
id
username
}
opsec_post_blocked
opsec_post_bypassed
opsec_post_bypass_user {
id
username
}
is_interactive_task
interactive_task_type
has_intercepted_response
tasks(where: {is_interactive_task: {_eq: false}}, order_by: {id: asc}) {
id
comment
agent_task_id
commentOperator{
username
}
@@ -137,6 +176,7 @@ export const taskingDataFragment = gql`
domain
ip
mythictree_groups
pid
}
completed
subtask_group_name
@@ -166,6 +206,7 @@ export const taskingDataFragment = gql`
tasks(order_by: {id: asc}) {
id
}
has_intercepted_response
}
response_count
tags {
@@ -182,8 +223,8 @@ export const taskingDataFragment = gql`
}
`;
export const createTaskingMutation = gql`
mutation createTasking($callback_id: Int, $callback_ids: [Int], $command: String!, $params: String!, $files: [String], $token_id: Int, $tasking_location: String, $original_params: String, $parameter_group_name: String, $parent_task_id: Int, $is_interactive_task: Boolean, $interactive_task_type: Int) {
createTask(callback_id: $callback_id, callback_ids: $callback_ids, command: $command, params: $params, files: $files, token_id: $token_id, tasking_location: $tasking_location, original_params: $original_params, parameter_group_name: $parameter_group_name, parent_task_id: $parent_task_id, is_interactive_task: $is_interactive_task, interactive_task_type: $interactive_task_type) {
mutation createTasking($callback_id: Int, $callback_ids: [Int], $command: String!, $params: String!, $files: [String], $token_id: Int, $tasking_location: String, $original_params: String, $parameter_group_name: String, $parent_task_id: Int, $is_interactive_task: Boolean, $interactive_task_type: Int, $payload_type: String) {
createTask(callback_id: $callback_id, callback_ids: $callback_ids, command: $command, params: $params, files: $files, token_id: $token_id, tasking_location: $tasking_location, original_params: $original_params, parameter_group_name: $parameter_group_name, parent_task_id: $parent_task_id, is_interactive_task: $is_interactive_task, interactive_task_type: $interactive_task_type, payload_type: $payload_type) {
status
id
error
@@ -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,53 +7,52 @@ 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}
if(tabId.includes("fileBrowser")) {
return Number(tabId.split("fileBrowser")[0]);
}else if(tabId.includes("interact")){
return Number(tabId.split("interact")[0]);
}else if(tabId.includes("processBrowser")) {
return Number(tabId.split("processBrowser")[0]);
}else if(tabId.includes("customFileBasedBrowser")) {
return Number(tabId.split("customFileBasedBrowser")[0]);
}else if(tabId.includes("customProcessBasedBrowser")) {
return Number(tabId.split("customProcessBasedBrowser")[0]);
} else {
console.log("unknown tab type", tabId);
return 0;
}
}));
}
export function Callbacks({me}) {
const [openCallbackImport, setOpenCallbackImport] = React.useState(false);
const [topDisplay, setTopDisplay] = React.useState('table');
const [newDataForTab, setNewDataForTab] = React.useState({});
const [openTabs, setOpenTabs] = React.useState([]);
const [clickedTabId, setClickedTabId] = React.useState('');
const [clickedTabId, setClickedTabIdValue] = React.useState('');
const openTabRef = React.useRef([]);
const callbackTableGridRef = React.useRef();
const [callbackTableSplitSizes, setCallbackTableSplitSizes] = React.useState([30, 70]);
const setClickedTabId = (tabID) => {
if(callbackTableGridRef.current){
let tabIDNumber = getCallbackIdFromClickedTab(tabID);
let rowIndex = callbackTableGridRef.current?.props?.itemData?.items?.findIndex((e) => {
return e[0]?.props?.rowData?.id === tabIDNumber
});
if(rowIndex >= 0){
callbackTableGridRef.current?.scrollToItem({rowIndex: rowIndex, align: "end", columnIndex: 0})
}
}
setClickedTabIdValue(tabID);
setNewDataForTab((prev) => {return {...prev, [tabID]: false}});
}
useEffect(() => {
const oldTabs = localStorage.getItem('openTabs');
if (oldTabs !== undefined && oldTabs !== null) {
@@ -72,12 +67,26 @@ export function Callbacks({me}) {
console.log('failed to parse oldTabs', error);
}
}
const oldSizes = localStorage.getItem("callbackTableSplitSizes");
if (oldSizes) {
try{
setCallbackTableSplitSizes(JSON.parse(oldSizes));
}catch(error){
console.log("failed to parse callback table split sizes");
}
}
}, []);
useEffect( () => {
openTabRef.current = openTabs;
}, [openTabs])
const onOpenTab = React.useRef( (tabData) => {
let found = false;
openTabRef.current = openTabRef.current.map( (tab) => {
if(tab.tabID === tabData.tabID){
return {...tabData};
}
return {...tab};
})
openTabRef.current.forEach((tab) => {
if (tab.tabID === tabData.tabID) found = true;
});
@@ -86,10 +95,29 @@ export function Callbacks({me}) {
const tabs = [...openTabRef.current, { ...tabData }];
localStorage.setItem('openTabs', JSON.stringify(tabs));
setOpenTabs(tabs);
} else {
setOpenTabs([...openTabRef.current]);
}
localStorage.setItem('clickedTab', tabData.tabID);
setClickedTabId(tabData.tabID);
});
const onOpenTabs = React.useRef( (tabData) => {
let currentTabs = [...openTabRef.current];
for(let i = 0; i < tabData.length; i++){
let found = false;
currentTabs.forEach((tab) => {
if (tab.tabID === tabData[i].tabID) found = true;
});
if (!found) {
currentTabs = [...currentTabs, { ...tabData[i] }];
}
}
localStorage.setItem('openTabs', JSON.stringify(currentTabs));
setOpenTabs(currentTabs);
localStorage.setItem('clickedTab', tabData[0].tabID);
setClickedTabId(tabData[0].tabID);
});
const onEditTabDescription = React.useCallback( (tabInfo, description) => {
const tabs = openTabs.map((t) => {
@@ -108,6 +136,10 @@ export function Callbacks({me}) {
});
localStorage.setItem('openTabs', JSON.stringify(tabSet));
setOpenTabs(tabSet);
if(tabSet.length === 0){
setClickedTabId("0");
localStorage.removeItem("clickedTab");
}
}, [openTabs]);
const onDragTab = ({selected, toLeftOf}) => {
//console.log("onDragTab in CallbacksTabs", selected, toLeftOf);
@@ -136,7 +168,7 @@ export function Callbacks({me}) {
newOpenTabList.push(openTabs[targetTabIndex]);
newOpenTabList.push(openTabs[i]);
}
setClickedTabId(openTabs[targetTabIndex].tabID)
} else {
newOpenTabList.push(openTabs[i]);
@@ -169,121 +201,73 @@ export function Callbacks({me}) {
}
},
];
const onDragEnd = ({ destination, source }) => {
// dropped outside the list
if (!destination) return;
const newItems = reorder(openTabs, source.index, destination.index);
setOpenTabs(newItems);
localStorage.setItem('openTabs', JSON.stringify(newItems));
};
return (
<>
<SpeedDialWrapper setTopDisplay={setTopDisplay} />
<Split direction="vertical" sizes={[30, 70]} minSize={[0,0]} style={{ height: "100%" }}>
<div className="bg-gray-base">
<CallbacksTop topDisplay={topDisplay} onOpenTab={onOpenTab.current} me={me}/>
<Split direction="vertical"
sizes={callbackTableSplitSizes}
minSize={[0,0]}
onDragEnd={(sizes) => localStorage.setItem('callbackTableSplitSizes', JSON.stringify(sizes))}
style={{ height: "100%" }}>
<div style={{display: "flex", flexDirection: "row-reverse"}}>
<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}
onOpenTab={onOpenTab.current}
onOpenTabs={onOpenTabs.current}
me={me} clickedTabId={clickedTabId}/>
</div>
<div className="bg-gray-mid">
<div >
<CallbacksTabs
onCloseTab={onCloseTab}
onEditTabDescription={onEditTabDescription}
key={'callbackstabs'}
clickedTabId={clickedTabId}
setClickedTabId={setClickedTabId}
openTabs={openTabs}
onDragTab={onDragTab}
me={me}
onDragEnd={onDragEnd}
contextMenuOptions={contextMenuOptions}
newDataForTab={newDataForTab}
setNewDataForTab={setNewDataForTab}
/>
</div>
</Split>
</>
);
}
/*
<div style={{ maxWidth: '100%', height: '100%', flexDirection: 'column'}}>
<React.Fragment>
<SpeedDialWrapper setTopDisplay={setTopDisplay} heights={heights} onSubmitHeights={onSubmitHeights} />
<div style={{flexGrow: 1, flexBasis: heights.top, height: heights.top }}>
<CallbacksTop topDisplay={topDisplay} onOpenTab={onOpenTab.current} heights={heights} me={me}/>
</div>
<div style={{ flexGrow: 1, flexBasis: heights.bottom, height: heights.bottom }}>
<CallbacksTabs
onCloseTab={onCloseTab}
onEditTabDescription={onEditTabDescription}
tabHeight={heights.bottom}
maxHeight={heights.bottom}
key={'callbackstabs'}
clickedTabId={clickedTabId}
openTabs={openTabs}
onDragTab={onDragTab}
me={me}
contextMenuOptions={contextMenuOptions}
/>
</div>
</React.Fragment>
</div>
*/
function SpeedDialWrapperPreMemo({ setTopDisplay }) {
const [open, setOpen] = React.useState(false);
const [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" }}
open={open}
style={{ marginTop:"35px" }}
direction='down'>
{actions.map((action) => (
<SpeedDialAction
arrow
className={classes.speedDialAction}
key={action.name}
TooltipClasses={{ ".MuiTooltip-tooltip": classes.tooltip,
".MuiTooltip-tooltipArrow": classes.arrow,
}}
icon={action.icon}
tooltipTitle={action.name}
onClick={action.onClick}
/>
))}
</StyledSpeedDial>
</React.Fragment>
);
}
const SpeedDialWrapper = React.memo(SpeedDialWrapperPreMemo);
}
@@ -1,16 +1,11 @@
import React, {useRef, useEffect, useState, useMemo, useContext} from 'react';
import React, {useEffect, useState, useMemo, useContext} from 'react';
import {DrawC2PathElementsFlowWithProvider} from './C2PathDialog';
import {Button} from '@mui/material';
import ButtonGroup from '@mui/material/ButtonGroup';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import Paper from '@mui/material/Paper';
import Grow from '@mui/material/Grow';
import Popper from '@mui/material/Popper';
import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import {useMutation } from '@apollo/client';
import {saveSvgAsPng} from 'save-svg-as-png';
import {hideCallbackMutation, removeEdgeMutation, addEdgeMutation} from './CallbackMutations';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
import {MythicSelectFromListDialog} from '../../MythicComponents/MythicSelectFromListDialog';
@@ -25,6 +20,9 @@ import InputLabel from '@mui/material/InputLabel';
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;
@@ -91,8 +89,8 @@ const GraphViewOptions = ({viewConfig, setViewConfig}) => {
evt.stopPropagation();
setDropdownOpen((prevOpen) => !prevOpen);
};
const handleMenuItemClick = (event, index) => {
options[index].click();
const handleMenuItemClick = (event, click) => {
click();
setDropdownOpen(false);
};
const options = [
@@ -129,7 +127,7 @@ const GraphViewOptions = ({viewConfig, setViewConfig}) => {
};
return (
<div >
<ButtonGroup variant="contained" ref={dropdownAnchorRef} aria-label="split button" style={{marginTop: "10px"}} color="primary">
<ButtonGroup variant="contained" ref={dropdownAnchorRef} aria-label="split button" style={{marginTop: "0px", marginLeft: "25px"}} color="primary">
<Button size="small" color="primary" aria-controls={dropdownOpen ? 'split-button-menu' : undefined}
aria-expanded={dropdownOpen ? 'true' : undefined}
aria-haspopup="menu"
@@ -138,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"
@@ -159,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"
@@ -183,40 +181,33 @@ const GraphViewOptions = ({viewConfig, setViewConfig}) => {
</FormControl>
}
<Popper open={dropdownOpen} anchorEl={dropdownAnchorRef.current} transition role={undefined} style={{zIndex: 200}}>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
}}
>
<Paper style={{backgroundColor: theme.palette.mode === 'dark' ? theme.palette.primary.dark : theme.palette.primary.light, color: "white"}}>
<ClickAwayListener onClickAway={handleClose}>
<MenuList id="split-button-menu">
{options.map((option, index) => (
<MenuItem
key={option.name}
onClick={(event) => handleMenuItemClick(event, index)}
>
{option.name}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
<ClickAwayListener onClickAway={handleClose}>
<Dropdown
isOpen={dropdownAnchorRef.current}
onOpen={setDropdownOpen}
externallyOpen={dropdownOpen}
menu={
options.map((option, index) => (
<DropdownMenuItem
key={option.name}
disabled={option.disabled}
onClick={(event) => handleMenuItemClick(event, option.click)}
>
{option.name}
</DropdownMenuItem>
))
}
/>
</ClickAwayListener>
</div>
)
}
export function CallbacksGraph({}){
const theme = useTheme();
export function CallbacksGraph({onOpenTab}){
const callbacks = useContext(CallbacksContext);
const callbackgraphedges = useContext(CallbackGraphEdgesContext);
const onOpenTab = useContext(OnOpenTabContext);
//used for creating a task to do a link command
const [loadingSettings, setLoadingSettings] = React.useState(true);
const [filterOptions, setFilterOptions] = React.useState({});
const [linkCommands, setLinkCommands] = React.useState([]);
const [openParametersDialog, setOpenParametersDialog] = React.useState(false);
const [openSelectLinkCommandDialog, setOpenSelectLinkCommandDialog] = React.useState(false);
@@ -331,7 +322,6 @@ export function CallbacksGraph({}){
{
title: 'Interact',
onClick: function(node){
console.log(node);
onOpenTab({tabType: "interact", tabID: node.callback_id + "interact", callbackID: node.callback_id});
}
},
@@ -376,14 +366,40 @@ export function CallbacksGraph({}){
}
},
]}, [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"} />}
/>
}
@@ -408,8 +424,9 @@ export function CallbacksGraph({}){
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>
);
}
File diff suppressed because it is too large. Load diff
@@ -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>
);
}
@@ -1,9 +1,8 @@
import React, {useCallback, useEffect, useContext} from 'react';
import {Button} from '@mui/material';
import {IconButton} from '@mui/material';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
import { MythicDisplayTextDialog} from '../../MythicComponents/MythicDisplayTextDialog';
import {MythicModifyStringDialog} from '../../MythicComponents/MythicDialog';
import ButtonGroup from '@mui/material/ButtonGroup';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import KeyboardIcon from '@mui/icons-material/Keyboard';
import LockIcon from '@mui/icons-material/Lock';
@@ -11,55 +10,34 @@ import WifiIcon from '@mui/icons-material/Wifi';
import InsertLinkTwoToneIcon from '@mui/icons-material/InsertLinkTwoTone';
import {C2PathDialog} from './C2PathDialog';
import {snackActions} from '../../utilities/Snackbar';
import Paper from '@mui/material/Paper';
import Grow from '@mui/material/Grow';
import Popper from '@mui/material/Popper';
import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import {
hideCallbackMutation,
lockCallbackMutation,
unlockCallbackMutation,
updateIPsCallbackMutation,
exportCallbackConfigQuery
} from './CallbackMutations';
import {useMutation, useLazyQuery } from '@apollo/client';
import {useMutation } from '@apollo/client';
import SnoozeIcon from '@mui/icons-material/Snooze';
import AccountTreeIcon from '@mui/icons-material/AccountTree';
import {useTheme} from '@mui/material/styles';
import LockOpenIcon from '@mui/icons-material/LockOpen';
import EditIcon from '@mui/icons-material/Edit';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {faQuestion, faSkullCrossbones, faFolderOpen, faList, faRobot} from '@fortawesome/free-solid-svg-icons';
import {faQuestion, faSkullCrossbones, faRobot} from '@fortawesome/free-solid-svg-icons';
import {faLinux, faApple, faWindows, faChrome, faAndroid} from '@fortawesome/free-brands-svg-icons';
import {DetailedCallbackTable} from './DetailedCallbackTable';
import InfoIcon from '@mui/icons-material/Info';
import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip';
import {TaskFromUIButton} from './TaskFromUIButton';
import { MythicSelectFromRawListDialog } from '../../MythicComponents/MythicSelectFromListDialog';
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
import { areEqual } from 'react-window';
import {CallbackGraphEdgesContext, OnOpenTabContext} from './CallbacksTop';
import Moment from 'react-moment';
import moment from 'moment';
import WidgetsIcon from '@mui/icons-material/Widgets';
import {ModifyCallbackMythicTreeGroupsDialog} from "./ModifyCallbackMythicTreeGroupsDialog";
import {GetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
import TerminalIcon from '@mui/icons-material/Terminal';
import ImportExportIcon from '@mui/icons-material/ImportExport';
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, toggleLock, updateDescription, setOpenHideMultipleDialog, setOpenTaskMultipleDialog}) =>{
const dropdownAnchorRef = React.useRef(null);
export const CallbacksTableIDCell = React.memo(({rowData, callbackDropdown}) =>{
const theme = useTheme();
const dropdownAnchorRef = React.useRef(null);
const onOpenTab = useContext(OnOpenTabContext);
const [openMetaDialog, setOpenMetaDialog] = React.useState(false);
const [dropdownOpen, setDropdownOpen] = React.useState(false);
const [openEditDescriptionDialog, setOpenEditDescriptionDialog] = React.useState(false);
const [openTaskingButton, setOpenTaskingButton] = React.useState(false);
const [openEditMythicTreeGroupsDialog, setOpenEditMythicTreeGroupsDialog] = React.useState(false);
const taskingData = React.useRef({"parameters": "", "ui_feature": "callback_table:exit"});
const interactType = GetMythicSetting({setting_name: "interactType", default_value: "interact"})
const [rowDataStatic, setRowDataStatic] = React.useState(rowData);
React.useEffect( () => {
let update = false;
@@ -81,272 +59,84 @@ export const CallbacksTableIDCell = React.memo(({rowData, toggleLock, 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 [lockCallback] = useMutation(lockCallbackMutation, {
update: (cache, {data}) => {
if(data.updateCallback.status === "success"){
snackActions.success("Locked callback");
}else{
snackActions.warning(data.updateCallback.error);
const proxyMessage = useCallback(() => {
let message = [];
for(let i = 0; i < rowDataStatic?.callbackports?.length || 0; i++){
switch(rowDataStatic.callbackports[i].port_type){
case "socks":
if(rowDataStatic.callbackports[i].username === ""){
message.push(`socks: ${rowDataStatic.callbackports[i].local_port}`);
} else {
message.push(`socks: ${rowDataStatic.callbackports[i].local_port}, ${rowDataStatic.callbackports[i].username}:${rowDataStatic.callbackports[i].password}`);
}
break;
case "rpfwd":
if(rowDataStatic.callbackports[i].username === ""){
message.push(`rpfwd: ${rowDataStatic.callbackports[i].local_port}->${rowDataStatic.callbackports[i].remote_ip}:${rowDataStatic.callbackports[i].remote_port}`);
} else {
message.push(`pfwd: ${rowDataStatic.callbackports[i].local_port}->${rowDataStatic.callbackports[i].remote_ip}:${rowDataStatic.callbackports[i].remote_port}, ${rowDataStatic.callbackports[i].username}:${rowDataStatic.callbackports[i].password}`);
}
break;
case "interactive":
message.push(`interactive: ${rowDataStatic.callbackports[i].local_port}`);
break;
}
},
onError: data => {
console.log(data);
snackActions.warning(data);
}
});
const [unlockCallback] = useMutation(unlockCallbackMutation, {
update: (cache, {data}) => {
if(data.updateCallback.status === "success"){
snackActions.success("Unlocked callback");
}else{
snackActions.warning(data.updateCallback.error);
}
},
onError: data => {
console.log(data);
snackActions.warning(data);
}
});
const editDescriptionSubmit = (description) => {
if(description === ""){
updateDescription({description: rowDataStatic.payload.description, callback_display_id: rowDataStatic.display_id});
} else {
updateDescription({description, callback_display_id: rowDataStatic.display_id});
}
}
const handleDropdownToggle = (evt) => {
evt.stopPropagation();
setDropdownOpen((prevOpen) => !prevOpen);
};
const localOnOpenTab = (tabType) => {
if(tabType === "interact"){
onOpenTab({tabType, tabID: rowDataStatic.id + tabType, callbackID: rowDataStatic.id, displayID: rowDataStatic.display_id});
}else if(tabType === "processBrowser"){
onOpenTab({tabType, tabID: rowDataStatic.host, callbackID: rowDataStatic.id, displayID: rowDataStatic.display_id});
}else{
onOpenTab({tabType, tabID: rowDataStatic.id + tabType, callbackID: rowDataStatic.id, displayID: rowDataStatic.display_id});
}
}
const handleMenuItemClick = (event, index) => {
options[index].click(event);
setDropdownOpen(false);
return message.join("\n");
}, [rowDataStatic]);
const handleDropdownToggle = (event) => {
event.stopPropagation();
callbackDropdown({rowDataStatic, event});
};
const handleClose = (event) => {
if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
return;
}
setDropdownOpen(false);
};
const [hideCallback] = useMutation(hideCallbackMutation, {
update: (cache, {data}) => {
if(data.updateCallback.status === "success"){
snackActions.success("Hiding callback");
}else{
snackActions.warning(data.updateCallback.error);
}
},
onError: data => {
console.log(data);
}
});
const localToggleLock = () => {
if(rowDataStatic.locked){
unlockCallback({variables: {callback_display_id: rowDataStatic.display_id}})
}else{
lockCallback({variables: {callback_display_id: rowDataStatic.display_id}})
}
const localOnOpenTab = () => {
onOpenTab({tabType: interactType, tabID: rowDataStatic.id + interactType, callbackID: rowDataStatic.id, displayID: rowDataStatic.display_id});
}
const [exportConfig] = useLazyQuery(exportCallbackConfigQuery, {
fetchPolicy: "no-cache",
onCompleted: (data) => {
if(data.exportCallbackConfig.status === "success"){
const dataBlob = new Blob([data.exportCallbackConfig.config], {type: 'text/plain'});
const ele = document.getElementById("download_config");
if(ele !== null){
ele.href = URL.createObjectURL(dataBlob);
ele.download = rowDataStatic.agent_callback_id + ".json";
ele.click();
}else{
const element = document.createElement("a");
element.id = "download_config";
element.href = URL.createObjectURL(dataBlob);
element.download = rowDataStatic.agent_callback_id + ".json";
document.body.appendChild(element);
element.click();
}
}else{
snackActions.error("Failed to export configuration: " + data.exportCallbackConfig.error);
}
},
onError: (data) => {
console.log(data);
snackActions.error("Failed to export configuration: " + data.message)
}
})
const options = [
{name: 'Hide Callback', icon: <VisibilityOffIcon style={{color: theme.palette.warning.main, paddingRight: "5px"}}/>, click: (evt) => {
evt.stopPropagation();
hideCallback({variables: {callback_display_id: rowDataStatic.display_id}});
}},
{
name: "Hide Multiple", icon: <VisibilityOffIcon style={{paddingRight: "5px"}}/>, click: (evt) => {
setOpenHideMultipleDialog(true);
}
},
{
name: "Exit Callback", icon: <FontAwesomeIcon icon={faSkullCrossbones} style={{color: theme.errorOnMain, cursor: "pointer", marginRight: "10px"}} />, click: (evt) => {
taskingData.current = {"parameters": "", "ui_feature": "callback_table:exit", "getConfirmation": true, acceptText: "exit"};
setOpenTaskingButton(true);
}
},
{
name: "Task Multiple", icon: <FontAwesomeIcon icon={faList} style={{cursor: "pointer", marginRight: "10px"}} />, click: (evt) => {
setOpenTaskMultipleDialog({open: true, data: rowDataStatic});
}
},
{name: 'File Browser', icon: <FontAwesomeIcon icon={faFolderOpen} style={{color: theme.folderColor, cursor: "pointer", marginRight: "10px"}} />, click: (evt) => {
evt.stopPropagation();
localOnOpenTab("fileBrowser");
}},
{name: 'Process Browser', icon: <AccountTreeIcon style={{paddingRight: "5px"}}/>, click: (evt) => {
evt.stopPropagation();
localOnOpenTab("processBrowser");
}},
{name: 'Split Tasking', icon: <KeyboardIcon style={{paddingRight: "5px"}}/>, click: (evt) => {
evt.stopPropagation();
localOnOpenTab("interactSplit");
}},
{name: "Console View", icon: <TerminalIcon style={{paddingRight: "5px"}}/>, click: (evt) => {
evt.stopPropagation();
localOnOpenTab("interactConsole");
}
},
{name: rowDataStatic.locked ? 'Unlock (Locked by ' + rowDataStatic.locked_operator.username + ')' : 'Lock Callback', icon: rowDataStatic.locked ? (<LockOpenIcon style={{color: theme.successOnMain, paddingRight: "5px"}}/>) : (<LockIcon style={{color: theme.errorOnMain, paddingRight: "5px"}} />), click: (evt) => {
evt.stopPropagation();
localToggleLock();
}},
{name: "Edit Description", icon: <EditIcon style={{paddingRight: "5px"}} />, click: (evt) => {
evt.stopPropagation();
setOpenEditDescriptionDialog(true);
}},
{name: "Expand Callback", icon: <OpenInNewIcon style={{paddingRight: "5px"}} />, click: (evt) => {
evt.stopPropagation();
window.open("/new/callbacks/" + rowDataStatic.display_id, "_blank").focus();
}},
{name: "Export Callback", icon: <ImportExportIcon style={{paddingRight: "5px"}} />, click: (evt) => {
evt.stopPropagation();
exportConfig({variables: {agent_callback_id: rowDataStatic.agent_callback_id}});
}},
{name: "View Metadata", icon: <InfoIcon style={{color: theme.infoOnMain, paddingRight: "5px"}} />, click: (evt) => {
evt.stopPropagation();
setOpenMetaDialog(true);
}},
{name: "Modify Groupings", icon: <WidgetsIcon style={{paddingRight: "5px"}} />, click: (evt) => {
evt.stopPropagation();
setOpenEditMythicTreeGroupsDialog(true);
}}
];
let defaultInteractIcon = <KeyboardIcon style={{paddingRight: "5px"}}/>;
if(interactType === "interactSplit"){
defaultInteractIcon = <VerticalSplitIcon style={{paddingRight: "5px"}}/>;
} else if(interactType === "interactConsole"){
defaultInteractIcon = <TerminalIcon style={{paddingRight: "5px"}}/>;
}
return (
<div>
<ButtonGroup
color={rowDataStatic.integrity_level > 2 ? "error" : "primary"}
ref={dropdownAnchorRef}
aria-label="split button"
<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()}}
>
<Button style={{padding: "0 10px 0 10px"}} color={rowDataStatic.integrity_level > 2 ? "error" : "primary"} variant="contained"
onClick={(evt) => {evt.stopPropagation();localOnOpenTab("interact")}}>
{ rowDataStatic.locked ? (<LockIcon fontSize="large" style={{color: theme.errorOnMain, marginRight: "10px"}} />):(<KeyboardIcon fontSize="large" style={{marginRight: "10px"}}/>) }
{rowDataStatic.display_id}
</Button>
<Button
style={{margin: 0, padding: 0}}
variant="contained"
aria-controls={dropdownOpen ? 'split-button-menu' : undefined}
aria-expanded={dropdownOpen ? 'true' : undefined}
color={rowDataStatic.integrity_level > 2 ? "error" : "primary"}
aria-haspopup="menu"
onClick={handleDropdownToggle}
>
<ArrowDropDownIcon/>
</Button>
</ButtonGroup>
<Popper open={dropdownOpen} anchorEl={dropdownAnchorRef.current} role={undefined} transition style={{zIndex: 200}}>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement === 'bottom' ? 'top left' : 'top center',
}}
>
<Paper className={"dropdownMenuColored"} elevation={5}>
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
<MenuList id="split-button-menu">
{options.map((option, index) => (
<MenuItem
key={option.name}
onClick={(event) => handleMenuItemClick(event, index)}
>
{option.icon}{option.name}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
{openTaskingButton &&
<TaskFromUIButton ui_feature={taskingData.current?.ui_feature || " "}
callback_id={rowDataStatic.id}
display_id={rowDataStatic.display_id}
parameters={taskingData.current?.parameters || ""}
openDialog={taskingData.current?.openDialog || false}
getConfirmation={taskingData.current?.getConfirmation || false}
acceptText={taskingData.current?.acceptText || "YES"}
selectCallback={taskingData.current?.selectCallback || false}
onTasked={() => setOpenTaskingButton(false)}/>
}
{openMetaDialog &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openMetaDialog}
onClose={()=>{setOpenMetaDialog(false);}}
innerDialog={<DetailedCallbackTable onClose={()=>{setOpenMetaDialog(false);}} callback_id={rowDataStatic.id} />}
/>
{rowDataStatic.locked ? (<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>
}
{openEditDescriptionDialog &&
<MythicDialog
fullWidth={true}
open={openEditDescriptionDialog}
onClose={() => {setOpenEditDescriptionDialog(false);}}
innerDialog={
<MythicModifyStringDialog title={"Edit Callback's Description"}
onClose={() => {setOpenEditDescriptionDialog(false);}}
value={rowDataStatic.description}
onSubmit={editDescriptionSubmit}
/>
}
/>
}
{openEditMythicTreeGroupsDialog &&
<MythicDialog
fullWidth={true}
maxWidth={"lg"}
open={openEditMythicTreeGroupsDialog}
onClose={() => {setOpenEditMythicTreeGroupsDialog(false);}}
innerDialog={
<ModifyCallbackMythicTreeGroupsDialog callback_id={rowDataStatic.id}
onClose={() => {setOpenEditMythicTreeGroupsDialog(false);}} />
}
/>
{rowDataStatic.callbackports.length > 0 &&
<MythicStyledTooltip title={proxyMessage()}>
<FontAwesomeIcon icon={faSocks} size="lg" style={{color: theme.palette.success.main}}/>
</MythicStyledTooltip>
}
</div>
)
@@ -357,22 +147,42 @@ 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.includes("54 years")){
return "Streaming Now"
}else if(newTime === "a few seconds"){
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"){
return ""
}
if(rowData.last_checkin === "1970-01-01T00:00:00"){
return (
<>
{rowData.dead &&
<MythicStyledTooltip title={"Based on callback's last checkin and sleep info, it's likely dead"}>
<FontAwesomeIcon disabled icon={faSkullCrossbones} style={{
color: theme.palette.error.main, cursor: "pointer", marginRight: "10px",}} />
</MythicStyledTooltip>
}
{"Streaming Now"}
</>
)
}
return (
<div>
<div style={{display: "flex", alignItems: "center"}}>
{rowData.dead &&
<MythicStyledTooltip title={"Based on callback's last checkin and sleep info, it's likely dead"}>
<FontAwesomeIcon icon={faSkullCrossbones} style={{
color: theme.palette.error.main, cursor: "pointer", marginRight: "10px",}} />
</MythicStyledTooltip>
}
<Moment filter={adjustOutput} interval={1000} parse={"YYYY-MM-DDTHH:mm:ss.SSSSSSZ"}
withTitle
titleFormat={"YYYY-MM-DD HH:mm:ss"}
@@ -386,12 +196,7 @@ export const CallbacksTableLastCheckinCell = React.memo( ({rowData, cellData}) =
}, areEqual);
export const CallbacksTablePayloadTypeCell = React.memo( ({rowData}) => {
return (
<MythicStyledTooltip title={rowData?.payload?.payloadtype?.name}>
<img
style={{width: "35px", height: "35px"}}
src={"/static/" + rowData?.payload?.payloadtype?.name + ".svg"}
/>
</MythicStyledTooltip>
rowData?.payload?.payloadtype?.name
)
}, areEqual)
export const CallbacksTableIPCell = React.memo(({cellData, rowData}) => {
@@ -435,16 +240,16 @@ export const CallbacksTableIPCell = React.memo(({cellData, rowData}) => {
}, [cellData]);
return (
<>
<div style={{display: "flex", alignItems: "center"}}>
<div style={{display: "inline-flex", alignItems: "center", height: "100%"}}>
{options.length > 1 &&
<MythicStyledTooltip title={"Adjust Displayed"}>
<UnfoldMoreIcon onClick={onClick} style={{paddingTop: "5px", cursor: "pointer"}} />
<UnfoldMoreIcon onClick={onClick} style={{paddingTop: "5px", cursor: "pointer", width: "unset"}} />
</MythicStyledTooltip>
}
{displayIP}
</div>
{openPickIP &&
<MythicDialog fullWidth={true} open={openPickIP} onClose={() => {setOpenPickIP(false);}}
<MythicDialog fullWidth={true} open={openPickIP} onClose={() => {setOpenPickIP(false);}}
innerDialog={
<MythicSelectFromRawListDialog
onClose={() => {setOpenPickIP(false);}}
@@ -505,10 +310,13 @@ export const CallbacksTableC2Cell = React.memo(({rowData}) => {
//look at all of the edges in myEdges and see if there are any edges that share a source/destination in props.callbackgraphedges that are _not_ in myEdges so far
const newEdges = initialCallbackGraphEdges?.reduce( (prev, edge) => {
//looking to see if we should add 'edge' to our list of relevant edges
if(prev.includes(edge)){return [...prev]}
const alreadyIncludes = prev.filter( (e) => e.id === edge.id);
if(alreadyIncludes.length > 0){
return [...prev]
}
//look through all of the previous edges we know about and see if there's a matching source/destination id with the new edge
const matching = prev.filter( (e) => {
if(e.source.id === edge.source.id || e.source.id === edge.destination.id || e.destination.id === edge.source.id ){
if(e.source.id === edge.source.id || e.source.id === edge.destination.id || e.destination.id === edge.source.id || e.destination.id === edge.destination.id ){
if(activeOnly){
if(edge.end_timestamp === null) { return true}
else{return false}
@@ -518,12 +326,12 @@ export const CallbacksTableC2Cell = React.memo(({rowData}) => {
return false;
});
if(matching.length > 0){
return [...prev, edge];
return [...prev, {...edge}];
}else{
return [...prev];
}
}, [...myEdges]) || [];
foundMore = newEdges.length > myEdges;
foundMore = newEdges.length > myEdges.length;
myEdges = [...newEdges];
}
return myEdges;
@@ -536,6 +344,7 @@ export const CallbacksTableC2Cell = React.memo(({rowData}) => {
}, [initialCallbackGraphEdges, localRowData]);
useEffect( () => {
//determine if there are any active routes left at all
//console.log(localRowData.display_id, callbackgraphedges)
const activeRoutes = callbackgraphedges.filter( (edge) => {
if(!edge.c2profile.is_p2p && edge.end_timestamp === null){
return true;
@@ -588,26 +397,26 @@ export const CallbacksTableOSCell = React.memo( ({rowData, cellData}) => {
const [openOSDialog, setOpenOSDialog] = React.useState(false);
const getOSIcon = useCallback( () => {
if(rowData?.payload?.payloadtype?.agent_type !== "agent"){
return <FontAwesomeIcon icon={faRobot} size="2x" style={{cursor: "pointer"}} onClick={displayOSInfo} />
return <FontAwesomeIcon icon={faRobot} style={{cursor: "pointer"}} onClick={displayOSInfo} />
}
switch(rowData.payload.os.toLowerCase()){
case "windows":
return <FontAwesomeIcon icon={faWindows} size="2x" style={{cursor: "pointer"}} onClick={displayOSInfo} />
return <FontAwesomeIcon icon={faWindows} style={{cursor: "pointer"}} onClick={displayOSInfo} />
case "linux":
case "centos":
case "redhat":
case "debian":
case "fedora":
case "freebsd":
return <FontAwesomeIcon icon={faLinux} size="2x" style={{cursor: "pointer"}} onClick={displayOSInfo} />
return <FontAwesomeIcon icon={faLinux} style={{cursor: "pointer"}} onClick={displayOSInfo} />
case "macos":
return <FontAwesomeIcon icon={faApple} size="2x" style={{cursor: "pointer"}} onClick={displayOSInfo}/>
return <FontAwesomeIcon icon={faApple} style={{cursor: "pointer"}} onClick={displayOSInfo}/>
case "chrome":
return <FontAwesomeIcon icon={faChrome} size="2x" style={{cursor: "pointer"}} onClick={displayOSInfo} />
return <FontAwesomeIcon icon={faChrome} style={{cursor: "pointer"}} onClick={displayOSInfo} />
case "android":
return <FontAwesomeIcon icon={faAndroid} size="2x" style={{cursor: "pointer"}} onClick={displayOSInfo} />
return <FontAwesomeIcon icon={faAndroid} style={{cursor: "pointer"}} onClick={displayOSInfo} />
default:
return <FontAwesomeIcon icon={faQuestion} size="2x" style={{cursor: "pointer"}} onClick={displayOSInfo} />
return <FontAwesomeIcon icon={faQuestion} style={{cursor: "pointer"}} onClick={displayOSInfo} />
}
}, [rowData?.payload?.os]);
const displayOSInfo = React.useCallback( () => {
@@ -645,15 +454,18 @@ export const CallbacksTableSleepCell = React.memo( ({rowData, cellData, updateSl
return null
}
return (
<div>
<div style={{height: "100%", display: "flex", alignItems: "center"}}>
<SnoozeIcon onClick={onOpenSleepDialog}
style={{color: cellData === "" ? theme.palette.warning.main : theme.palette.info.main, cursor: "pointer"}}
/>
{ openSleepDialog &&
<MythicDialog fullWidth={true} open={openSleepDialog} onClose={() => {setOpenSleepDialog(false);}}
<MythicDialog fullWidth={true} open={openSleepDialog} maxWidth={"md"}
style={{height: "100%"}}
onClose={() => {setOpenSleepDialog(false);}}
innerDialog={
<MythicModifyStringDialog title={"View Sleep Information"} multiline maxRows={10}
onClose={() => {setOpenSleepDialog(false);}}
<MythicModifyStringDialog title={"View / Edit Sleep Information - This doesn't issue tasks to the agent, but helps alive/dead tracking"}
maxRows={20}
onClose={() => {setOpenSleepDialog(false);}}
value={cellData}
onSubmit={editSleepSubmit} />
}
@@ -662,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,20 +1,30 @@
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, onEditTabDescription, contextMenuOptions, me}) {
export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clickedTabId, setClickedTabId, onEditTabDescription, contextMenuOptions, me, newDataForTab, setNewDataForTab}) {
const mountedRef = React.useRef(true);
const [value, setValue] = React.useState(0);
const handleChange = (event, newValue) => {
setValue(newValue);
//console.log(newValue);
//console.log("clickedTab", newValue, openTabs[newValue]);
if(openTabs[newValue] === undefined){return}
localStorage.setItem('clickedTab', openTabs[newValue].tabID);
setClickedTabId(openTabs[newValue].tabID);
};
React.useEffect( () => {
return() => {
@@ -24,107 +34,166 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, o
}, [])
const onCloseTabLocal = ({ tabID, index }) => {
if (index > 0) {
setClickedTabId(openTabs[index-1].tabID);
setValue(index - 1);
} else {
//console.log("closing index", index)
} else if(openTabs.length > 0) {
//console.log("closing index length > 0", index)
setClickedTabId(openTabs[0].tabID);
setValue(0);
}
onCloseTab({ tabID, index });
};
useEffect(() => {
//console.log(clickedTabId);
//console.log(clickedTabId, openTabs);
for (let i = 0; i < openTabs.length; i++) {
//console.log("openTabs[i]", i, openTabs[i]);
if (openTabs[i].tabID === clickedTabId) {
//console.log("seting value", i);
//console.log("setting value", i);
setValue(i);
return;
}
}
}, [clickedTabId, openTabs]);
const [collapseTaskRequest, setCollapseTaskRequest] = React.useState({})
const modifiedInteractContextMenuOptions = [...contextMenuOptions,
{
name: 'Collapse All Tasks',
click: ({event, index}) => {
setCollapseTaskRequest((prevState) => {
if(prevState[index] !== undefined){
prevState[index] += 1;
return {...prevState};
} else {
return { [index]: 0};
}
});
}
},]
return (
<div style={{width: "100%", display: 'flex', flexDirection: 'column', flexGrow: 1, height: "100%" }}>
<AppBar color='default' position='static' className={"no-box-shadow"}>
<Tabs
value={value}
onChange={handleChange}
indicatorColor='primary'
textColor='primary'
variant='scrollable'
scrollButtons='auto'
style={{ maxWidth: '100%', width: '100%' }}
aria-label='scrollable auto tabs example'>
{openTabs.map((tab, index) => {
switch (tab.tabType) {
case 'interact':
return (
<CallbacksTabsTaskingLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
);
case 'interactSplit':
return (
<CallbacksTabsTaskingSplitLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
)
case 'interactConsole':
return (
<CallbacksTabsTaskingConsoleLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
)
case 'fileBrowser':
return (
<CallbacksTabsFileBrowserLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
);
case 'processBrowser':
return (
<CallbacksTabsProcessBrowserLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
);
default:
return null;
}
})}
</Tabs>
<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) => {
@@ -141,9 +210,11 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, o
key={'tabpanel' + tab.tabID + tab.tabType}
onCloseTab={onCloseTabLocal}
tabInfo={tab}
setNewDataForTab={setNewDataForTab}
value={value}
index={index}
me={me}
collapseTaskRequest={collapseTaskRequest}
parentMountedRef={mountedRef}
/>
);
@@ -159,6 +230,7 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, o
key={'tabpanel' + tab.tabID + tab.tabType}
onCloseTab={onCloseTabLocal}
tabInfo={tab}
setNewDataForTab={setNewDataForTab}
value={value}
index={index}
me={me}
@@ -177,12 +249,32 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, o
key={'tabpanel' + tab.tabID + tab.tabType}
onCloseTab={onCloseTabLocal}
tabInfo={tab}
setNewDataForTab={setNewDataForTab}
value={value}
index={index}
me={me}
parentMountedRef={mountedRef}
/>
)
case 'customFileBasedBrowser':
return (
<CallbacksTabsCustomFileBasedBrowserPanel
style={{
height: '100%',
maxHeight: '100%',
position: 'relative',
overflow: 'auto',
}}
onCloseTab={onCloseTabLocal}
key={'tabpanel' + tab.tabID + tab.tabType + tab?.customBrowser?.name}
tabInfo={tab}
setNewDataForTab={setNewDataForTab}
value={value}
index={index}
me={me}
parentMountedRef={mountedRef}
/>
);
case 'fileBrowser':
return (
<CallbacksTabsFileBrowserPanel
@@ -195,10 +287,12 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, o
onCloseTab={onCloseTabLocal}
key={'tabpanel' + tab.tabID + tab.tabType}
tabInfo={tab}
setNewDataForTab={setNewDataForTab}
value={value}
index={index}
me={me}
parentMountedRef={mountedRef}
baseUIFeature={"file_browser"}
/>
);
case 'processBrowser':
@@ -213,6 +307,7 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, o
onCloseTab={onCloseTabLocal}
key={'tabpanel' + tab.tabID + tab.tabType}
tabInfo={tab}
setNewDataForTab={setNewDataForTab}
value={value}
index={index}
me={me}
@@ -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);
};
@@ -20,6 +20,16 @@ import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip'
import Split from 'react-split';
import {subscriptionCallbackTokens} from "./CallbacksTabsTaskingInput";
import {CallbacksTabsTaskingInputTokenSelect} from "./CallbacksTabsTaskingInputTokenSelect";
import KeyboardReturnIcon from '@mui/icons-material/KeyboardReturn';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import {b64DecodeUnicode} from "./ResponseDisplay";
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
import PlaylistRemoveIcon from '@mui/icons-material/PlaylistRemove';
import {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 {
@@ -28,6 +38,8 @@ const fileDataFragment = gql`
task_id
filemeta {
id
agent_file_id
filename_text
}
tags {
tagtype {
@@ -35,10 +47,12 @@ const fileDataFragment = gql`
color
id
}
id
}
host
id
can_have_children
has_children
success
full_path_text
name_text
@@ -56,20 +70,32 @@ 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
}
}
`;
const folderQuery = gql`
${fileDataFragment}
query myFolderQuery($parent_path_text: String!) {
mythictree(
query myFolderQuery($parent_path_text: String!, $parents: [String]) {
children: mythictree(
where: { parent_path_text: { _eq: $parent_path_text }, tree_type: {_eq: "file"} }
order_by: { can_have_children: asc, name: asc }
) {
...fileObjData
}
parents: mythictree(
where: { full_path_text: { _in: $parents }, tree_type: {_eq: "file"} }
order_by: { can_have_children: asc, name: asc }
) {
...fileObjData
}
self: mythictree(
where: { full_path_text: { _eq: $parent_path_text }, tree_type: {_eq: "file"} }
order_by: { can_have_children: asc, name: asc }
) {
...fileObjData
}
}
`;
const fileDataSubscription = gql`
@@ -84,7 +110,40 @@ const fileDataSubscription = gql`
}
}
`;
const fileBrowserTaskSub = gql`
subscription fileBrowserTasks($now: timestamp!, $callback_id: Int!){
task_stream(
batch_size: 10,
cursor: {initial_value: {timestamp: $now}},
where: {tasking_location: {_eq: "file_browser"}, callback_id: {_eq: $callback_id}}
){
id
status
completed
}
}
`;
export const getAllParentNodes = (node) => {
let linuxPathSeparatorIndex = node.full_path_text.indexOf("/");
let windowsPathSeparatorIndex = node.full_path_text.indexOf("\\");
let newNodes = [node.full_path_text];
if(linuxPathSeparatorIndex !== -1){
let pieces = node.full_path_text.split("/");
newNodes.push("/");
for(let i = 0; i < pieces.length; i++){
let newElement = pieces.slice(0, i).join("/");
if(newElement === ""){continue}
newNodes.push(newElement)
}
}
if(windowsPathSeparatorIndex !== -1){
let pieces = node.full_path_text.split("\\");
for(let i = 0; i < pieces.length; i++){
newNodes.push(pieces.slice(0, i).join("\\"))
}
}
return newNodes;
}
export function CallbacksTabsFileBrowserLabel(props) {
const [description, setDescription] = React.useState('File Browser: ' + props.tabInfo.displayID);
const [openEditDescriptionDialog, setOpenEditDescriptionDialog] = React.useState(false);
@@ -116,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}
@@ -126,7 +185,7 @@ export function CallbacksTabsFileBrowserLabel(props) {
}}
innerDialog={
<MythicModifyStringDialog
title={"Edit Tab's Description"}
title={"Edit Tab's Description - Displays as one line"}
onClose={() => {
setOpenEditDescriptionDialog(false);
}}
@@ -139,27 +198,50 @@ export function CallbacksTabsFileBrowserLabel(props) {
</React.Fragment>
);
}
export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) => {
const [fromNow, setFromNow] = React.useState((new Date()));
export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNewDataForTab, baseUIFeature }) => {
const fromNow = React.useRef(getSkewedNow());
const [backdropOpen, setBackdropOpen] = React.useState(false);
const treeRootDataRef = React.useRef({}); // hold all of the actual data
const [treeAdjMtx, setTreeAdjMtx] = React.useState({}); // hold the simple adjacency matrix for parent/child relationships
// hold the simple adjacency matrix for parent/child relationships
const [treeAdjMtx, setTreeAdjMtx] = React.useState({});
const [selectedFolderData, setSelectedFolderData] = React.useState({
full_path_text: '.',
host: "",
id: "",
full_path_text: '',
host: tabInfo.host,
group: "Default",
parent_path_text: ""
});
const [showDeletedFiles, setShowDeletedFiles] = React.useState(false);
const [openTaskingButton, setOpenTaskingButton] = React.useState(false);
const taskingData = React.useRef({"parameters": "", "ui_feature": "file_browser:list"});
const taskingTableTopTypedDataRef = React.useRef({
path: "",
token: 0,
});
const autoTaskLsOnEmptyDirectoriesRef = React.useRef(false);
const taskingData = React.useRef({"parameters": "", "ui_feature": baseUIFeature +":list"});
const mountedRef = React.useRef(true);
const tableOpenedPathIdRef = React.useRef(0);
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
// create the top level data in the treeRootDataRef
let defaultGroup = "Default";
for(let i = 0; i < data.mythictree.length; i++){
let currentGroups = data.mythictree[i]?.["callback"]?.["mythictree_groups"] || ["Unknown Callbacks"];
try{
if(data.mythictree[i]?.['callback']?.['id'] === tabInfo.callbackID){
if(!currentGroups.includes(defaultGroup, 0)){
defaultGroup = currentGroups[0];
}
}
}catch(error){
console.log(error);
}
for(let j = 0; j < currentGroups.length; j++){
if(treeRootDataRef.current[currentGroups[j]] === undefined){
treeRootDataRef.current[currentGroups[j]] = {};
@@ -168,7 +250,17 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
// new host discovered
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]] = {};
}
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] = {...data.mythictree[i]}
if(treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] === undefined){
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] = {...data.mythictree[i]};
} else {
if(treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]].success === null){
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
@@ -192,11 +284,17 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
}, {...treeAdjMtx});
//console.log(treeRootDataRef.current);
setTreeAdjMtx(newMatrix);
setSelectedFolderData({
full_path_text: '',
host: tabInfo.host,
group: defaultGroup,
parent_path_text: "",
});
},
fetchPolicy: 'no-cache',
});
useSubscription(fileDataSubscription, {
variables: {now: fromNow},
variables: {now: fromNow.current},
fetchPolicy: "no-cache",
onData: ({data}) => {
for(let i = 0; i < data.data.mythictree_stream.length; i++){
@@ -209,7 +307,54 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
// new host discovered
treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]] = {};
}
treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]] = {...data.data.mythictree_stream[i]};
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
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"]].filemeta = treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]].filemeta.map(f => {
return {...f, filename_text: b64DecodeUnicode(f.filename_text)}
})
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"]){
setSelectedFolderData({...treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]],
group: currentGroups[j], fromHistory: selectedFolderData.fromHistory});
}
} else {
// we need to merge data in because we already have some info
let existingData = treeRootDataRef.current[currentGroups[j]][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.tags = [...existingData.tags, ...data.data.mythictree_stream[i].tags];
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};
}
}
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"]){
setSelectedFolderData({...existingData, group: currentGroups[j], fromHistory: selectedFolderData.fromHistory});
}
}
}
}
const newMatrix = data.data.mythictree_stream.reduce( (prev, cur) => {
@@ -231,9 +376,57 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
return prev;
}, {...treeAdjMtx});
setTreeAdjMtx(newMatrix);
if(index !== value){
setNewDataForTab((prev) => {return {...prev, [tabInfo.tabID]: true}});
}
}
})
useSubscription(fileBrowserTaskSub, {
variables: {now: fromNow.current, callback_id: tabInfo.callbackID},
fetchPolicy: "no-cache",
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.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);
@@ -242,23 +435,74 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
notifyOnNetworkStatusChange: true,
onCompleted: (data) => {
//console.log("getFolderData", data)
let mythictree = [...data.parents, ...data.children];
snackActions.dismiss();
// add in all of the raw data
for(let i = 0; i < data.mythictree.length; i++){
let currentGroups = data.mythictree[i]?.["callback"]?.["mythictree_groups"] || ["Unknown Callbacks"];
for(let i = 0; i < mythictree.length; i++){
let currentGroups = mythictree[i]?.["callback"]?.["mythictree_groups"] || ["Unknown Callbacks"];
for(let j = 0; j < currentGroups.length; j++){
if(treeRootDataRef.current[currentGroups[j]] === undefined){
treeRootDataRef.current[currentGroups[j]] = {};
}
if( treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]] === undefined) {
if( treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]] === undefined) {
// new host discovered
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]] = {};
treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]] = {};
}
if(treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]][mythictree[i]["full_path_text"]] === undefined){
// first time we're seeing this file data, just add it
treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]][mythictree[i]["full_path_text"]] = {...mythictree[i]};
treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]][mythictree[i]["full_path_text"]].filemeta = treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]][mythictree[i]["full_path_text"]].filemeta.map(f => {
return {...f, filename_text: b64DecodeUnicode(f.filename_text)}
})
if(selectedFolderData.group === currentGroups[j] && selectedFolderData.host === mythictree[i]["host"] &&
selectedFolderData.full_path_text === mythictree[i]["full_path_text"]){
setSelectedFolderData({...treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]][mythictree[i]["full_path_text"]],
group: currentGroups[j], fromHistory: selectedFolderData.fromHistory});
}
} else {
// we need to merge data in because we already have some info
let existingData = treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]][mythictree[i]["full_path_text"]];
if(existingData.task_id <= mythictree[i].task_id){
existingData.deleted = mythictree[i].deleted;
}
if( (existingData.success === null || !existingData.success) && mythictree[i].success !== null){
existingData.success = mythictree[i].success;
existingData.task_id = mythictree[i].task_id;
}
existingData.comment += mythictree[i].comment;
existingData.tags = [...existingData.tags, ...mythictree[i].tags];
existingData.has_children = mythictree[i].has_children || existingData.has_children;
existingData.metadata.size = mythictree[i].metadata.size;
existingData.metadata.access_time = mythictree[i].metadata.access_time;
existingData.metadata.modify_time = mythictree[i].metadata.modify_time;
//console.log("updating permissions", "existing", existingData.metadata.permissions, "new", mythictree[i].metadata)
if(mythictree[i].metadata.permissions !== undefined && mythictree[i].metadata.permissions !== null){
if(Array.isArray(existingData.metadata.permissions) && Array.isArray(mythictree[i].metadata.permissions)){
existingData.metadata.permissions = [...existingData.metadata.permissions, ...mythictree[i].metadata.permissions];
} else if(Array.isArray(existingData.metadata.permissions) !== Array.isArray(mythictree[i].metadata.permissions)) {
// had array/dict and got mismatched array/dict, so just take newer one
existingData.metadata.permissions = mythictree[i].metadata.permissions;
} else {
existingData.metadata.permissions = {...existingData.metadata.permissions, ...mythictree[i].metadata.permissions};
}
}
let newfileData = mythictree[i].filemeta.map(f => {
return {...f, filename_text: b64DecodeUnicode(f.filename_text)};
})
existingData.filemeta = [...existingData.filemeta, ...newfileData]
existingData.filemeta.sort((a,b) => a.id > b.id ? 1 : -1);
treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]][mythictree[i]["full_path_text"]] = {...existingData};
if(selectedFolderData.group === currentGroups[j] && selectedFolderData.host === mythictree[i]["host"] &&
selectedFolderData.full_path_text === mythictree[i]["full_path_text"]){
setSelectedFolderData({...existingData,
group: currentGroups[j],
fromHistory: selectedFolderData.fromHistory});
}
}
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] = {...data.mythictree[i]}
}
}
// create the top level data in the adjacency matrix
const newMatrix = data.mythictree.reduce( (prev, cur) => {
const newMatrix = mythictree.reduce( (prev, cur) => {
let currentGroups = cur?.["callback"]?.["mythictree_groups"] || ["Unknown Callbacks"];
for(let j = 0; j < currentGroups.length; j++) {
if (prev[currentGroups[j]] === undefined) {
@@ -280,41 +524,121 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
setTreeAdjMtx(newMatrix);
//console.log("just set treeAdjMtx, about to close backdrop")
setBackdropOpen(false);
if(data.self.length > 0){
//setSelectedFolderData({...selectedFolderData, task_id: data.self[0].task_id, success: data.self[0].success});
// this path exists, let's see if we have data for it and if the user wants us to auto-issue an ls for the path
let newAllData = Object.keys(newMatrix[selectedFolderData.group]?.[selectedFolderData.host]?.[data.self[0].full_path_text] || {});
if(autoTaskLsOnEmptyDirectoriesRef.current){
if(newAllData.length === 0 && data.self[0].full_path_text !== "" && data.self[0].success === null){
onListFilesButtonFromTableWithNoEntries()
}
}
} else {
// we couldn't find the path specified, so we must not have data for it, so check if the user wants us to auto issue an ls
if(autoTaskLsOnEmptyDirectoriesRef.current){
// don't want to auto ls when we get to the root/host object in the view
if(selectedFolderData.full_path_text !== ""){
onListFilesButtonFromTableWithNoEntries()
}
}
}
},
});
const onSetTableData = useCallback((nodeData) => {
//console.log("setTableData", nodeData);
setSelectedFolderData(nodeData);
}, []);
const fetchFolderData = useCallback((nodeData) => {
const fetchFolderData = useCallback((nodeData, fromHistory) => {
let parentNodes = getAllParentNodes(nodeData);
getFolderData({
variables: { parent_path_text: nodeData.full_path_text},
variables: { parent_path_text: nodeData.full_path_text, parents: parentNodes},
});
setBackdropOpen(true);
tableOpenedPathIdRef.current = nodeData.id;
setSelectedFolderData(nodeData);
setSelectedFolderData({...nodeData, fromHistory: fromHistory});
}, []);
const fetchParentFolderData = (nodeData) => {
let parentNodes = getAllParentNodes(nodeData);
getFolderData({
variables: { parent_path_text: nodeData.parent_path_text, parents: parentNodes},
});
setBackdropOpen(true);
let parentData = treeRootDataRef.current[nodeData.group][nodeData.host][nodeData.parent_path_text];
parentData.group = nodeData.group;
tableOpenedPathIdRef.current = {
group: parentData.group,
host: parentData.host,
full_path_text: parentData.full_path_text
};
setSelectedFolderData({...parentData, fromHistory: false});
};
const localSelectedToken = React.useRef("");
const onChangeSelectedToken = (token) => {
localSelectedToken.current = token;
}
const onListFilesButton = ({ fullPath, callback_id, callback_display_id, token }) => {
let path = fullPath;
if(path === ""){
path = ".";
}
taskingData.current = ({
"token": token,
"parameters": {path: fullPath, full_path: fullPath, host: selectedFolderData.host, file: ""},
"ui_feature": "file_browser:list", callback_id, callback_display_id});
"parameters": {path: path, full_path: path, host: selectedFolderData.host, file: ""},
"ui_feature": baseUIFeature + ":list", callback_id, callback_display_id});
setOpenTaskingButton(true);
};
const onListFilesButtonFromTableWithNoEntries = () => {
taskingData.current = ({
"token": taskingTableTopTypedDataRef.current.token === "Default Token" ? 0 : taskingTableTopTypedDataRef.current.token,
"parameters": {
path: taskingTableTopTypedDataRef.current.path,
full_path: taskingTableTopTypedDataRef.current.path,
host: selectedFolderData.host,
file: ""
},
"ui_feature": baseUIFeature +":list",
callback_id: tabInfo.callbackID,
callback_display_id: tabInfo.displayID});
setOpenTaskingButton(true);
}
const onUploadFileButton = ({ fullPath, callback_id, callback_display_id, token }) => {
taskingData.current = ({
"token": token,
"parameters": {path: fullPath, full_path: fullPath, host: selectedFolderData.host},
"ui_feature": "file_browser:upload", "openDialog": true,
"ui_feature": baseUIFeature + ":upload", "openDialog": true,
callback_id, callback_display_id
});
setOpenTaskingButton(true);
};
const multipleTasks = React.useRef([]);
const onTaskRowActions = useCallback( (tasks) => {
let updatedTasks = [];
for(let i = 0; i < tasks.length; i++){
updatedTasks.push({
"token": localSelectedToken.current,
"parameters": {
host: selectedFolderData.host,
path: tasks[i].path,
full_path: tasks[i].full_path,
file: tasks[i].filename,
},
"ui_feature": tasks[i].uifeature,
openDialog: tasks[i].openDialog,
getConfirmation: tasks[i].getConfirmation,
callback_id: tasks[i].callback_id,
callback_display_id: tasks[i].callback_display_id});
}
multipleTasks.current = updatedTasks;
taskingData.current = multipleTasks.current.pop()
setOpenTaskingButton(true);
}, [selectedFolderData, localSelectedToken.current]);
React.useEffect( () => {
if(!openTaskingButton){
if(multipleTasks.current.length > 0){
taskingData.current = multipleTasks.current.pop()
setOpenTaskingButton(true);
}
}
}, [openTaskingButton]);
const onTaskRowAction = useCallback(({ path, full_path, filename, uifeature, openDialog, getConfirmation, callback_id, callback_display_id }) => {
taskingData.current = ({
"token": localSelectedToken.current,
@@ -339,13 +663,26 @@ 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}) => {
let parentNodes = getAllParentNodes({full_path_text: path});
getFolderData({
variables: {parent_path_text: path, parents: parentNodes}
})
setBackdropOpen(true);
setSelectedFolderData({host, group, full_path_text: path, id: 0, success: null});
tableOpenedPathIdRef.current = {
group: group,
host: host,
full_path_text: path
};
}
return (
<MythicTabPanel index={index} value={value}>
<Split direction="horizontal" style={{width: "100%", height: "100%", display: "flex"}} sizes={[30, 70]} >
<Split direction="horizontal" style={{width: "100%", height: "100%", display: "flex", overflow: "hidden"}} sizes={[30, 70]} >
<div className="bg-gray-base" style={{display: "inline-flex"}}>
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={true}>
<CircularProgress color="inherit" />
@@ -355,23 +692,32 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
showDeletedFiles={showDeletedFiles}
treeRootData={treeRootDataRef.current}
treeAdjMatrix={treeAdjMtx}
selectedFolderData={selectedFolderData}
fetchFolderData={fetchFolderData}
setTableData={onSetTableData}
taskListing={taskListing}
tableOpenedPathId={tableOpenedPathIdRef.current}
getLoadedCommandForUIFeature={getLoadedCommandForUIFeature}
baseUIFeature={baseUIFeature}
/>
</div>
<div className="bg-gray-light" style={{display: "inline-flex"}}>
<div style={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
<div style={{ display: 'flex', flexDirection: 'column', flexGrow: 1, overflow: "hidden" }}>
<div style={{ flexGrow: 0 }}>
<FileBrowserTableTop
tabInfo={tabInfo}
taskingTableTopTypedDataRef={taskingTableTopTypedDataRef}
autoTaskLsOnEmptyDirectoriesRef={autoTaskLsOnEmptyDirectoriesRef}
onChangeSelectedToken={onChangeSelectedToken}
selectedFolderData={selectedFolderData}
onListFilesButton={onListFilesButton}
onUploadFileButton={onUploadFileButton}
toggleShowDeletedFiles={toggleShowDeletedFiles}
fetchParentFolderData={fetchParentFolderData}
fetchFolderData={fetchFolderData}
openDirectoryPath={openDirectoryPath}
baseUIFeature={baseUIFeature}
/>
</div>
<div style={{ flexGrow: 1 }}>
@@ -384,9 +730,14 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
onRowDoubleClick={fetchFolderData}
treeRootData={treeRootDataRef.current}
treeAdjMatrix={treeAdjMtx}
onListFilesButtonFromTableWithNoEntries={onListFilesButtonFromTableWithNoEntries}
selectedFolderData={selectedFolderData}
autoTaskLsOnEmptyDirectories={autoTaskLsOnEmptyDirectoriesRef.current}
onTaskRowAction={onTaskRowAction}
onTaskRowActions={onTaskRowActions}
getLoadedCommandForUIFeature={getLoadedCommandForUIFeature}
me={me}
baseUIFeature={baseUIFeature}
/>
</div>
</div>
@@ -409,28 +760,45 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
};
const FileBrowserTableTop = ({
selectedFolderData,
taskingTableTopTypedDataRef,
autoTaskLsOnEmptyDirectoriesRef,
onChangeSelectedToken,
onListFilesButton,
onUploadFileButton,
toggleShowDeletedFiles,
tabInfo
fetchParentFolderData,
fetchFolderData,
openDirectoryPath,
tabInfo,
baseUIFeature
}) => {
const autoTaskLsOnEmptyDirectories = useGetMythicSetting({
setting_name: "autoTaskLsOnEmptyDirectories", default_value: false
});
const [updateMythicSetting] = useSetMythicSetting();
const [fullPath, setFullPath] = React.useState('');
const selectedToken = React.useRef("Default Token");
const [tokenOptions, setTokenOptions] = React.useState([]);
const [placeHolder, setPlaceHolder] = React.useState(selectedFolderData.host);
const [showDeletedFiles, setLocalShowDeletedFiles] = React.useState(false);
const [history, setHistory] = React.useState([]);
const [historyIndex, setHistoryIndex] = React.useState(0);
const onChangePath = (_, value) => {
setFullPath(value);
taskingTableTopTypedDataRef.current.path = value;
taskingTableTopTypedDataRef.current.token = selectedToken;
};
const changeSelectedToken = (token) => {
onChangeSelectedToken(token);
if(token === "Default Token"){
selectedToken.current = "Default Token";
taskingTableTopTypedDataRef.current.token = selectedToken;
return;
}
if(token.token_id !== selectedToken.current.token_id){
selectedToken.current = token;
taskingTableTopTypedDataRef.current.token = selectedToken;
}
}
useSubscription(subscriptionCallbackTokens, {
@@ -438,11 +806,18 @@ const FileBrowserTableTop = ({
shouldResubscribe: true,
onData: ({data}) => {
setTokenOptions(data.data.callbacktoken);
if(data.data.callbacktoken.length === 0) {
onChangeSelectedToken("Default Token");
}
}
});
useEffect(() => {
if (selectedFolderData.full_path_text !== undefined) {
if(selectedFolderData.root) {
setFullPath("");
taskingTableTopTypedDataRef.current.path = "";
} else if (selectedFolderData.full_path_text !== undefined) {
setFullPath(selectedFolderData.full_path_text);
taskingTableTopTypedDataRef.current.path = selectedFolderData.full_path_text;
}
const groups = selectedFolderData?.callback?.mythictree_groups?.join(", ") || "";
if(groups.length > 0 ){
@@ -450,14 +825,53 @@ const FileBrowserTableTop = ({
} else {
setPlaceHolder(selectedFolderData.host + groups);
}
}, [selectedFolderData]);
const onLocalListFilesButton = () => {
if (fullPath === '') {
snackActions.warning('Must provide a path to list');
useEffect( () => {
if(selectedFolderData.fromHistory){
return;
}
onListFilesButton({ fullPath, token: selectedToken.current });
if(selectedFolderData.id !== ""){
if(history[0]?.full_path_text !== selectedFolderData.full_path_text){
// always add newest things to the bottom of the stack
setHistory([selectedFolderData, ...history]);
if(history.length > 20){
// pop off from the top (the oldest) if we get more than 50
setHistory([selectedFolderData, ...history.splice(history.length-1, 1)])
}
}
}
}, [selectedFolderData, history])
const moveIndexToPreviousListing = () => {
// we're getting closer to the end of the historyRef.current, the oldest listing
if(historyIndex >= history.length -1){
return
}
setHistoryIndex(historyIndex + 1);
fetchFolderData(history[historyIndex + 1], true);
}
const moveIndexToNextListing = () => {
// we're getting close to index 0, the newest listing
//console.log(historyIndex, history);
if(historyIndex <= 0){
return
}
setHistoryIndex(historyIndex - 1);
fetchFolderData(history[historyIndex - 1], true);
}
const onLocalListFilesButton = () => {
let tempPath = fullPath;
if (tempPath === "") {
tempPath = ".";
}
if(tempPath.length > 1){
if(tempPath[tempPath.length-1] === "/" || tempPath[tempPath.length-1] === "\\"){
let newFullPath = tempPath.slice(0, tempPath.length-1);
setFullPath(newFullPath);
onListFilesButton({ fullPath: newFullPath, token: selectedToken.current });
return
}
}
onListFilesButton({ fullPath: tempPath, token: selectedToken.current });
};
const onLocalUploadFileButton = () => {
onUploadFileButton({ fullPath, token: selectedToken.current });
@@ -466,28 +880,85 @@ const FileBrowserTableTop = ({
setLocalShowDeletedFiles(!showDeletedFiles);
toggleShowDeletedFiles(!showDeletedFiles);
};
const onLocalMoveUpDirectoryButton = () => {
if(selectedFolderData.parent_path_text !== ""){
fetchParentFolderData(selectedFolderData)
}
};
const onToggleAutoTaskLsOnEmptyDirectories = () => {
updateMythicSetting({setting_name: "autoTaskLsOnEmptyDirectories", value: !autoTaskLsOnEmptyDirectories});
if(autoTaskLsOnEmptyDirectories){
snackActions.info("No longer auto issuing listings for empty paths");
} else {
snackActions.success("Now starting to auto issue listings for empty paths");
}
}
const goToDirectory = () => {
if(fullPath === ""){return}
if(fullPath.length > 1){
if(fullPath[fullPath.length-1] === "/" || fullPath[fullPath.length-1] === "\\"){
let newFullPath = fullPath.slice(0, fullPath.length-1);
setFullPath(newFullPath);
openDirectoryPath({
group: selectedFolderData.group,
host: selectedFolderData.host,
path: newFullPath,
})
return
}
}
openDirectoryPath({
group:selectedFolderData.group,
host: selectedFolderData.host,
path: fullPath,
})
}
useEffect( () => {
autoTaskLsOnEmptyDirectoriesRef.current = autoTaskLsOnEmptyDirectories;
}, [autoTaskLsOnEmptyDirectories]);
return (
<Grid container spacing={0} style={{ paddingTop: '10px' }}>
<Grid item xs={12}>
<Grid size={12}>
<MythicTextField
placeholder={selectedFolderData.host}
value={fullPath}
onEnter={onLocalListFilesButton}
onEnter={goToDirectory}
onChange={onChangePath}
name={placeHolder}
InputProps={{
endAdornment: (
<React.Fragment>
<MythicStyledTooltip title={`Task current callback (${tabInfo["displayID"]}) to list contents`}>
<IconButton style={{ padding: '3px' }} onClick={onLocalListFilesButton} size="large">
<IconButton style={{ padding: '3px' }}
onClick={onLocalListFilesButton}
size="large">
<RefreshIcon color='info' />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={`Upload file to folder via current callback (${tabInfo["displayID"]})`}>
<IconButton style={{ padding: '3px' }} onClick={onLocalUploadFileButton} size="large">
<IconButton style={{ padding: '3px' }}
onClick={onLocalUploadFileButton}
size="large">
<CloudUploadIcon color="info" />
</IconButton>
</MythicStyledTooltip>
{autoTaskLsOnEmptyDirectories ? (
<MythicStyledTooltip title={"Currently tasking listing on empty directories, click to toggle off"} >
<IconButton style={{padding: "3px"}}
onClick={onToggleAutoTaskLsOnEmptyDirectories}
size={"large"}>
<PlaylistAddIcon color={"success"} ></PlaylistAddIcon>
</IconButton>
</MythicStyledTooltip>
) : (
<MythicStyledTooltip title={"Currently not tasking listing on empty directories, click to toggle on"} >
<IconButton style={{padding: "3px"}}
onClick={onToggleAutoTaskLsOnEmptyDirectories}
size={"large"}>
<PlaylistRemoveIcon color={"secondary"} ></PlaylistRemoveIcon>
</IconButton>
</MythicStyledTooltip>
)}
<MythicStyledTooltip title={showDeletedFiles ? 'Hide Deleted Files' : 'Show Deleted Files'}>
<IconButton
style={{ padding: '3px' }}
@@ -503,12 +974,44 @@ const FileBrowserTableTop = ({
</React.Fragment>
),
startAdornment: (
<React.Fragment>
{tokenOptions.length > 0 ? (
<CallbacksTabsTaskingInputTokenSelect options={tokenOptions} changeSelectedToken={changeSelectedToken}/>
) : null}
</React.Fragment>),
style: { padding: 0 },
<div style={{display: "inline-flex",
alignItems: "center",
borderRight: "1px solid grey",
marginRight: "10px",
padding: "0 5px 0 0"}}>
{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 }
onClick={moveIndexToPreviousListing}
color='info'
size="large">
<ArrowBackIcon />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={`Move to next listing`}>
<IconButton style={{ padding: '3px' }}
disabled={historyIndex <= 0}
onClick={moveIndexToNextListing}
size="large"
color='info'>
<ArrowForwardIcon />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Move up a directory"} >
<IconButton style={{padding: "0 0 0 0"}}
onClick={onLocalMoveUpDirectoryButton}
disabled={!selectedFolderData?.parent_path_text || selectedFolderData?.parent_path_text?.length === 0 || selectedFolderData.root || fullPath === ""}
>
<KeyboardReturnIcon style={{rotate: "90deg"}} ></KeyboardReturnIcon>
</IconButton>
</MythicStyledTooltip>
</div>),
style: { },
}}
/>
</Grid>
File diff suppressed because it is too large. Load diff
@@ -1,54 +1,176 @@
import React from 'react';
import FileBrowserVirtualTree from '../../MythicComponents/MythicFileBrowserVirtualTree';
import {FileBrowserVirtualTree} from '../../MythicComponents/MythicFileBrowserVirtualTree';
import {MythicDialog} from "../../MythicComponents/MythicDialog";
import {ViewCallbackMythicTreeGroupsDialog} from "./ViewCallbackMythicTreeGroupsDialog";
import ListIcon from '@mui/icons-material/List';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import {getAllParentNodes} from "./CallbacksTabsFileBrowser";
import {Dropdown, DropdownMenuItem, DropdownNestedMenuItem} from "../../MythicComponents/MythicNestedMenus";
import {copyStringToClipboard} from "../../utilities/Clipboard";
import FileCopyOutlinedIcon from '@mui/icons-material/FileCopyOutlined';
import {snackActions} from "../../utilities/Snackbar";
export const CallbacksTabsFileBrowserTree = ({ treeRootData, treeAdjMatrix, fetchFolderData, setTableData, taskListing, tableOpenedPathId, showDeletedFiles, tabInfo}) => {
export const getOpenIDFromNode = (node) => {
return `${node.group};${node.host};${node.full_path_text}`;
}
export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatrix, fetchFolderData, setTableData, taskListing, tableOpenedPathId, showDeletedFiles, tabInfo, selectedFolderData, baseUIFeature}) => {
const [openNodes, setOpenNodes] = React.useState({});
const groupName = React.useRef("");
const [openViewGroupsDialog, setOpenViewGroupDialog] = React.useState(false);
const lastOpenedNodeRef = React.useRef({group: "", host: "", full_path_text: ""});
const toggleNodeExpanded = (nodeId, nodeData) => {
//console.log("toggleNodeExpanded", nodeId, nodeData);
let lastOpenedNodeNewState = true;
let lastOpenedNodeID = getOpenIDFromNode(lastOpenedNodeRef.current);
if(!lastOpenedNodeRef.current?.has_children){
lastOpenedNodeNewState = false;
}
lastOpenedNodeRef.current = {...nodeData};
setTableData(nodeData);
fetchFolderData(nodeData);
setOpenNodes({
...openNodes,
[nodeId]: true
[getOpenIDFromNode(nodeData)]: true,
//[lastOpenedNodeID]: lastOpenedNodeNewState
});
};
const toggleNodeCollapsed = (nodeId, nodeData) => {
const toggleNodeCollapsed = (nodeId, nodeData) => {
setOpenNodes({
...openNodes,
[nodeId]: false
[getOpenIDFromNode(nodeData)]: false
});
};
const onSelectNode = (nodeId, nodeData) => {
const onSelectNode = React.useCallback( (nodeId, nodeData) => {
if(nodeData.root){
setTableData(nodeData);
}else if(nodeData.is_group){
groupName.current = nodeData.group;
setOpenViewGroupDialog(true);
}else {
//console.log(nodeData);
setTableData(nodeData);
//setTableData(nodeData);
toggleNodeExpanded(nodeId, nodeData);
}
};
}, [openNodes, setTableData]);
React.useEffect( () => {
let group = tableOpenedPathId.group;
let host = tableOpenedPathId.host;
let allPaths = [...getAllParentNodes(tableOpenedPathId),];
const additionalOpenNodes = allPaths.reduce( (prev, cur) => {
return {...prev, [getOpenIDFromNode({group: group, host: host, full_path_text: cur})]: true}
}, {});
setOpenNodes({
...openNodes,
[tableOpenedPathId]: true
[getOpenIDFromNode(tableOpenedPathId)]: true,
...additionalOpenNodes
});
}, [tableOpenedPathId]);
const contextMenuOptions = [
React.useEffect( () => {
let group = selectedFolderData.group;
let host = selectedFolderData.host;
let allPaths = [...getAllParentNodes(selectedFolderData)];
const additionalOpenNodes = allPaths.reduce( (prev, cur) => {
return {...prev, [getOpenIDFromNode({group: group, host: host, full_path_text: cur})]: true}
}, {});
setOpenNodes({
...openNodes,
[getOpenIDFromNode(selectedFolderData)]: true,
...additionalOpenNodes
});
}, [selectedFolderData]);
const contextMenuOptions= (callback_id, callback_display_id, node) => [
{
name: "Copy to Clipboard", icon: null, click: () => {}, type: "menu",
menuItems: [
{
name: 'Name', type: "item",
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
click: ({event}) => {
event.stopPropagation();
if(copyStringToClipboard(node.name_text)){
snackActions.success("Copied to clipboard");
}
},
},
{
name: 'Full Path', type: "item",
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
click: ({event}) => {
event.stopPropagation();
if(copyStringToClipboard(node.full_path_text)){
snackActions.success("Copied to clipboard");
}
},
},
{
name: 'Metadata', type: "item",
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
click: ({event}) => {
event.stopPropagation();
if(copyStringToClipboard(JSON.stringify(node?.metadata, null, 2))){
snackActions.success("Copied to clipboard");
}
},
},
]
},
{
name: 'Task Listing',
click: ({event, node, callback_id, callback_display_id}) => {
name: 'List', type: "item", icon: <ListIcon color="warning" style={{ paddingRight: '5px'}} />,
click: ({event}) => {
event.stopPropagation();
taskListing(node, callback_id, callback_display_id);
}
},
];
const [openContextMenu, setOpenContextMenu] = React.useState(false);
const contextMenuData = React.useRef({options: []});
const onContextMenu = React.useCallback( ({event, item, itemTreeData}) => {
event.preventDefault();
event.stopPropagation();
contextMenuData.current = {item, itemTreeData,
x: event.clientX,
y:event.clientY,
dropdownAnchorRef: event.currentTarget};
if(contextMenuData.current.item?.root){
}else if(contextMenuData.current.item?.is_group){
}else {
let options = [{
name: contextMenuData.current?.item?.name_text, icon: null, disabled: true, type: "item",
click: () => {}
}];
options.push(...contextMenuOptions(tabInfo["callbackID"], tabInfo["displayID"], contextMenuData.current?.item));
if(contextMenuData.current?.item?.callback?.["display_id"] !== tabInfo["displayID"]){
options.push({
name: "Original Callback: " + contextMenuData.current?.item?.callback?.["display_id"],
icon: null, click: () => {}, type: "menu",
menuItems: [
...contextMenuOptions(contextMenuData.current?.item?.callback?.id, contextMenuData.current?.item?.callback?.display_id, contextMenuData.current?.item)
]
})
}
contextMenuData.current.options = options;
setOpenContextMenu(true);
}
}, []);
const handleMenuItemClick = (event, click) => {
event.preventDefault();
event.stopPropagation();
click({event});
//contextMenuOptions[index].click({event,
// node: {...contextMenuData.current?.item, group: contextMenuData.current?.itemTreeData?.group, host: contextMenuData.current?.itemTreeData?.host},
// callback_id, callback_display_id
//});
setOpenContextMenu(false);
};
const handleClose = (event) => {
event.preventDefault();
event.stopPropagation();
setOpenContextMenu(false);
};
//console.log(selectedFolderData)
return(
<>
<FileBrowserVirtualTree
@@ -56,11 +178,13 @@ export const CallbacksTabsFileBrowserTree = ({ treeRootData, treeAdjMatrix, fetc
treeRootData={treeRootData}
treeAdjMatrix={treeAdjMatrix}
openNodes={openNodes}
selectedFolderData={selectedFolderData}
onSelectNode={onSelectNode}
onExpandNode={toggleNodeExpanded}
onCollapseNode={toggleNodeCollapsed}
contextMenuOptions={contextMenuOptions}
onContextMenu={onContextMenu}
tabInfo={tabInfo}
baseUIFeature={baseUIFeature}
/>
{openViewGroupsDialog &&
<MythicDialog
@@ -74,7 +198,51 @@ export const CallbacksTabsFileBrowserTree = ({ treeRootData, treeAdjMatrix, fetc
}
/>
}
{openContextMenu &&
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
<Dropdown
isOpen={contextMenuData.current.dropdownAnchorRef}
onOpen={setOpenContextMenu}
externallyOpen={openContextMenu}
absoluteX={contextMenuData.current.x}
absoluteY={contextMenuData.current.y}
anchorReference={"anchorPosition"}
style={{
//top: contextMenuData.current.y,
//left: contextMenuData.current.x
}}
menu={[
...contextMenuData.current?.options?.map((option, index) => (
option.type === 'item' ? (
<DropdownMenuItem
key={option.name}
disabled={option.disabled}
onClick={(event) => handleMenuItemClick(event, option.click)}
>
{option.icon} {option.name}
</DropdownMenuItem>
) : option.type === 'menu' ? (
<DropdownNestedMenuItem
label={option.name}
disabled={option.disabled}
menu={
option.menuItems.map((menuOption, indx) => (
<DropdownMenuItem
key={menuOption.name}
disabled={menuOption.disabled}
onClick={(event) => handleMenuItemClick(event, menuOption.click)}
>
{menuOption.icon}{menuOption.name}
</DropdownMenuItem>
))
}
/>
) : null))
]}/>
</ClickAwayListener>
}
</>
)
};
};
export const CallbacksTabsFileBrowserTree = React.memo(CallbacksTabsFileBrowserTreePreMemo);
@@ -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`
@@ -22,10 +23,12 @@ query callbacksAndFeatures{
pid
description
last_checkin
dead
payload {
payloadtype {
name
id
agent_type
}
}
mythictree_groups_string
@@ -33,7 +36,7 @@ query callbacksAndFeatures{
}`;
const columns = [
{ field: 'display_id', headerName: 'ID', width: 80, type: 'number', },
{ field: 'display_id', headerName: 'ID', width: 70, type: 'number', },
{
field: 'host',
headerName: 'Host',
@@ -48,7 +51,7 @@ const columns = [
field: 'pid',
headerName: 'PID',
type: 'number',
width: 80,
width: 70,
},
{
field: 'description',
@@ -58,14 +61,14 @@ const columns = [
{
field: 'ip',
headerName: 'IP',
flex: 1,
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;
}
}
},
@@ -73,15 +76,15 @@ 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} />,
},
{
field: "payload.payloadtype.name",
headerName: "Agent",
width: 40,
valueGetter: (params) => params.row.payload.payloadtype.name,
flex: 0.5,
valueGetter: (value, row) => row.payload.payloadtype.name,
renderCell: (params) => <CallbacksTablePayloadTypeCell rowData={params.row} />
},
{
@@ -90,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]);
@@ -115,7 +121,7 @@ const CustomSelectTable = ({initialData, selectedData}) => {
},
},
sorting: {
sortModel: [{ field: 'last_checkin', sort: 'asc' }],
sortModel: [sortModel],
},
}}
autoPageSize
@@ -170,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
@@ -183,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>
);
}
@@ -0,0 +1,159 @@
import React, {useContext} from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
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`
query callbacksAndFeatures{
callback(where: {active: {_eq: true}}, order_by: {id: asc}) {
id
display_id
host
ip
user
process_name
pid
description
last_checkin
dead
payload {
payloadtype {
name
id
agent_type
}
}
mythictree_groups_string
}
}`;
const columns = [
{ field: 'display_id', headerName: 'ID', width: 70, type: 'number', },
{
field: 'host',
headerName: 'Host',
flex: 0.5,
},
{
field: 'user',
headerName: 'User',
flex: 0.5,
},
{
field: 'pid',
headerName: 'PID',
type: 'number',
width: 70,
},
{
field: 'description',
headerName: 'Description',
flex: 1,
},
{
field: 'ip',
headerName: 'IP',
width: 100,
renderCell: (params) => <CallbacksTableIPCell rowData={params.row} cellData={params.row.ip} />,
sortable: false,
valueGetter: (value, row) => {
try{
return JSON.parse(row.ip)[0];
}catch(error){
return row.ip;
}
}
},
{
field: "last_checkin",
headerName: "Checkin",
width: 100,
valueGetter: (value, row) => new Date(row.last_checkin),
renderCell: (params) =>
<CallbacksTableLastCheckinCell rowData={params.row} />,
},
{
field: "payload.payloadtype.name",
headerName: "Agent",
flex: 0.5,
valueGetter: (value, row) => row.payload.payloadtype.name,
renderCell: (params) => <CallbacksTablePayloadTypeCell rowData={params.row} />
},
{
field: "mythictree_groups_string",
headerName: "Groups",
flex: 0.5,
}
];
const CustomSelectTable = ({initialData, selectedData, onRowClick}) => {
const [data, setData] = React.useState([]);
React.useEffect( () => {
setData(initialData.map(c => {
return {...c};
}));
}, [initialData]);
return (
<div style={{height: "calc(80vh)"}}>
<DataGrid
rows={data}
columns={columns}
initialState={{
pagination: {
paginationModel: {
},
},
sorting: {
sortModel: [{ field: 'display_id', sort: 'desc' }],
},
}}
autoPageSize
onRowClick={onRowClick}
density={"compact"}
/>
</div>
)
}
export function CallbacksTabsOpenMultipleDialog({onClose, tabType, onOpenTabs}) {
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 onRowClick = (rowData) => {
onOpenTabs([rowData.row]);
}
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}
/>
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
Close
</Button>
</DialogActions>
</React.Fragment>
);
}
@@ -1,13 +1,11 @@
import {MythicTabPanel, MythicTabLabel} from '../../MythicComponents/MythicTabPanel';
import React, {useEffect, useRef} from 'react';
import {gql, useQuery, useSubscription } from '@apollo/client';
import {gql, useQuery, useSubscription, useLazyQuery } from '@apollo/client';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
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';
@@ -19,15 +17,17 @@ import InputLabel from '@mui/material/InputLabel';
import Input from '@mui/material/Input';
import {ViewCallbackMythicTreeGroupsDialog} from "./ViewCallbackMythicTreeGroupsDialog";
import WidgetsIcon from '@mui/icons-material/Widgets';
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 {
comment
deleted
task_id
filemeta {
id
}
tags {
tagtype {
name
@@ -56,11 +56,11 @@ fragment treeObjData on mythictree {
`;
const treeSubscription = gql`
${treeFragment}
subscription liveData($now: timestamp!, $operation_id: Int!) {
subscription liveData($now: timestamp!) {
mythictree_stream(
batch_size: 1000,
cursor: {initial_value: {timestamp: $now}},
where: { operation_id: { _eq: $operation_id }, tree_type: {_eq: "process"} }
cursor: {initial_value: {timestamp: $now}},
where: {tree_type: {_eq: "process"}, deleted: {_eq: false} }
) {
...treeObjData
}
@@ -68,13 +68,38 @@ const treeSubscription = gql`
`;
const rootQuery = gql`
${treeFragment}
query myRootFolderQuery($operation_id: Int!) {
mythictree(where: { operation_id: { _eq: $operation_id }, tree_type: {_eq: "process"} }, order_by: {id: asc}) {
query myRootProcessesQuery {
mythictree(where: {parent_path_text: { _eq: "" }, tree_type: {_eq: "process"} }, order_by: {id: asc}) {
...treeObjData
}
}
`;
export const uniqueSplitString = "$&%^";
const treeHostQuery = gql`
${treeFragment}
query processesPerHostQuery($host: String!){
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);
@@ -105,19 +130,21 @@ 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={
<MythicModifyStringDialog title={"Edit Tab's Description"} onClose={() => {setOpenEditDescriptionDialog(false);}} value={description} onSubmit={editDescriptionSubmit} />
<MythicModifyStringDialog title={"Edit Tab's Description - Displays as one line"} onClose={() => {setOpenEditDescriptionDialog(false);}} value={description} onSubmit={editDescriptionSubmit} />
}
/>
}
</React.Fragment>
)
}
export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>{
const [fromNow, setFromNow] = React.useState((new Date()));
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);
const treeRootDataRef = React.useRef({}); // hold all the actual data
const [treeAdjMtx, setTreeAdjMtx] = React.useState({}); // hold the simple adjacency matrix for parent/child relationships
const [openTaskingButton, setOpenTaskingButton] = React.useState(false);
@@ -126,12 +153,39 @@ 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] = {"":{}}}
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"] ] = {}
}
newMatrix[group][host][ node["parent_path_text"] ][full_path_text] = 1
}
}
}
return newMatrix;
}
useQuery(rootQuery, {
variables: { operation_id: me?.user?.current_operation_id ||0},
onCompleted: (data) => {
// use an adjacency matrix but only for full_path_text -> children, not both directions
let defaultGroup = "Default";
for(let i = 0; i < data.mythictree.length; i++){
let currentGroups = data.mythictree[i]?.["callback"]?.["mythictree_groups"] || ["Unknown Callbacks"];
try{
if(data.mythictree[i]?.['callback']?.['id'] === tabInfo.callbackID){
if(!currentGroups.includes(defaultGroup, 0)){
defaultGroup = currentGroups[0];
}
}
}catch(error){
console.log(error);
}
for(let j = 0; j < currentGroups.length; j++){
if(treeRootDataRef.current[currentGroups[j]] === undefined){
treeRootDataRef.current[currentGroups[j]] = {};
@@ -140,39 +194,28 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>
// new host discovered
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]] = {};
}
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"] /*+ uniqueSplitString + data.mythictree[i]["callback_id"]*/] = {...data.mythictree[i]}
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]
}
}
// create the top level data in the adjacency matrix
const newMatrix = data.mythictree.reduce( (prev, cur) => {
let currentGroups = cur?.["callback"]?.["mythictree_groups"] || ["Unknown Callbacks"];
for(let j = 0; j < currentGroups.length; j++) {
if (prev[currentGroups[j]] === undefined) {
prev[currentGroups[j]] = {};
}
if (prev[currentGroups[j]][cur["host"]] === undefined) {
// the current host isn't tracked in the adjacency matrix, so add it
prev[currentGroups[j]][cur["host"]] = {}
}
if(cur["parent_path_text"] === ""){
if(prev[currentGroups[j]][cur['host']][""] === undefined){
prev[currentGroups[j]][cur['host']][""] = {}
}
prev[currentGroups[j]][cur['host']][""][cur["full_path_text"] /*+ uniqueSplitString + cur["callback_id"]*/] = 1;
continue
}
if (prev[currentGroups[j]][cur["host"]][cur["parent_path_text"] /*+ uniqueSplitString + cur["callback_id"]*/] === undefined) {
// the current parent's path isn't tracked, so add it and ourselves as children
prev[currentGroups[j]][cur["host"]][cur["parent_path_text"] /*+ uniqueSplitString + cur["callback_id"]*/] = {};
}
prev[currentGroups[j]][cur["host"]][cur["parent_path_text"] /*+ uniqueSplitString + cur["callback_id"]*/][cur["full_path_text"] /*+ uniqueSplitString + cur["callback_id"]*/] = 1;
}
return prev;
}, {...treeAdjMtx});
const newMatrix = getNewMatrix();
setTreeAdjMtx(newMatrix);
// first see if we can find a group that matches our host, if not, then we can do first of each
let groups = Object.keys(newMatrix).sort();
let groups = Object.keys(newMatrix).sort();
if(groups.length > 0){
if(groups.includes(defaultGroup)){
setSelectedGroup(defaultGroup);
const hosts = Object.keys(newMatrix[defaultGroup]).sort();
if(hosts.length > 0){
if(hosts.includes(tabInfo.host)){
setSelectedHost(tabInfo.host);
} else {
setSelectedHost(hosts[0]);
}
}
return
}
for(let i = 0; i < groups.length; i++){
const hosts = Object.keys(newMatrix[groups[i]]).sort();
if(hosts.length > 0){
@@ -192,8 +235,21 @@ 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, operation_id: me?.user?.current_operation_id ||0},
variables: {now: fromNow.current},
fetchPolicy: "no-cache",
onData: ({data}) => {
for(let i = 0; i < data.data.mythictree_stream.length; i++){
@@ -206,36 +262,85 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>
// new host discovered
treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]] = {};
}
treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"] /*+ uniqueSplitString + data.data.mythictree_stream[i]["callback_id"]*/] = {...data.data.mythictree_stream[i]};
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];
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 = data.data.mythictree_stream.reduce( (prev, cur) => {
let currentGroups = cur?.["callback"]?.["mythictree_groups"] || ["Unknown Callbacks"];
for(let j = 0; j < currentGroups.length; j++) {
if (prev[currentGroups[j]] === undefined) {
prev[currentGroups[j]] = {};
}
if (prev[currentGroups[j]][cur["host"]] === undefined) {
// the current host isn't tracked in the adjacency matrix, so add it
prev[currentGroups[j]][cur["host"]] = {}
}
if(cur["parent_path_text"] === ""){
if(prev[currentGroups[j]][cur['host']][""] === undefined){
prev[currentGroups[j]][cur['host']][""] = {}
}
prev[currentGroups[j]][cur['host']][""][cur["full_path_text"] /*+ uniqueSplitString + cur["callback_id"]*/] = 1;
continue
}
if (prev[currentGroups[j]][cur["host"]][cur["parent_path_text"] /*+ uniqueSplitString + cur["callback_id"]*/] === undefined) {
// the current parent's path isn't tracked, so add it and ourselves as children
prev[currentGroups[j]][cur["host"]][cur["parent_path_text"] /*+ uniqueSplitString + cur["callback_id"]*/] = {};
}
prev[currentGroups[j]][cur["host"]][cur["parent_path_text"] /*+ uniqueSplitString + cur["callback_id"]*/][cur["full_path_text"] /*+ uniqueSplitString + cur["callback_id"]*/] = 1;
}
return prev;
}, {...treeAdjMtx});
const newMatrix = getNewMatrix();
setTreeAdjMtx(newMatrix);
if(index !== value){
setNewDataForTab((prev) => {return {...prev, [tabInfo.tabID]: true}});
}
}
});
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;
}
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 {
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);
}
const getHostProcessesQuery = useMythicLazyQuery(treeHostQuery, {
fetchPolicy: 'no-cache',
})
const onListFilesButton = () => {
taskingData.current = ({parameters: "",
@@ -264,6 +369,33 @@ 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}})
.then(({data}) => getHostProcessesQuerySuccess(data)).catch(({data}) => console.log(data));
setBackdropOpen(true);
}, [selectedHost]);
React.useEffect( () => {
return() => {
mountedRef.current = false;
@@ -272,8 +404,23 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>
}, [])
return (
<MythicTabPanel index={index} value={value} >
<div style={{display: "flex", flexGrow: 1, overflowY: "auto"}}>
<div style={{display: "flex", flexGrow: 1, overflowY: "auto", position: "relative"}}>
<div style={{width: "100%", display: "flex", flexDirection: "column", flexGrow: 1}}>
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute",}} invisible={false}>
<div style={{
borderRadius: "4px",
border: "1px solid black",
padding: "5px",
backgroundColor: "rgba(37,37,37,0.92)", color: "white",
alignItems: "center",
display: "flex", flexDirection: "column"}}>
<CircularProgress color="inherit" />
<Typography variant={"h5"}>
Gathering Processes from database for {selectedHost}...
</Typography>
</div>
</Backdrop>
<ProcessBrowserTableTop
onListFilesButton={onListFilesButton}
tabInfo={tabInfo}
@@ -284,6 +431,8 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>
updateSelectedGroup={updateSelectedGroup}
groupOptions={treeRootDataRef.current}
hostOptions={treeRootDataRef.current[selectedGroup] || {}}
expandOrCollapseAll={expandOrCollapseAll}
setExpandOrCollapseAll={setExpandOrCollapseAll}
/>
<CallbacksTabsProcessBrowserTable
showDeletedFiles={showDeletedFiles}
@@ -293,7 +442,9 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>
treeAdjMatrix={treeAdjMtx}
host={selectedHost}
group={selectedGroup}
expandOrCollapseAll={expandOrCollapseAll}
onTaskRowAction={onTaskRowAction}
getLoadedCommandForUIFeature={getLoadedCommandForUIFeature}
me={me}/>
</div>
@@ -304,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>
@@ -318,7 +470,9 @@ const ProcessBrowserTableTop = ({
host,
group,
hostOptions,
groupOptions
groupOptions,
expandOrCollapseAll,
setExpandOrCollapseAll
}) => {
const theme = useTheme();
const [showDeletedFiles, setLocalShowDeletedFiles] = React.useState(false);
@@ -338,9 +492,12 @@ const ProcessBrowserTableTop = ({
const handleGroupChange = (event) => {
updateSelectedGroup(event.target.value);
}
const onLocalExpandOrCollapseAllButton = () => {
setExpandOrCollapseAll(!expandOrCollapseAll);
}
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
@@ -352,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>
}
@@ -375,18 +532,13 @@ const ProcessBrowserTableTop = ({
<MythicStyledTooltip title="Task Callback to List Processes">
<IconButton style={{padding: "3px"}} onClick={onLocalListFilesButton} size="large"><RefreshIcon style={{color: theme.palette.info.main}}/></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>
<MythicStyledTooltip title={expandOrCollapseAll ? "Collapse all processes" : "Expand all processes"} >
<IconButton style={{padding: "3px"}}
onClick={onLocalExpandOrCollapseAllButton} size={"large"}>
<ExpandIcon color={expandOrCollapseAll ? "info" : "success"} />
</IconButton>
</MythicStyledTooltip>
<div style={{paddingRight: "20px"}} />
</React.Fragment>
}
>
@@ -2,23 +2,13 @@ import React, {useCallback, useMemo} from 'react';
import { alpha, IconButton } from "@mui/material";
import {useLazyQuery, gql, useMutation } from '@apollo/client';
import { MythicDialog, MythicViewJSONAsTableDialog, MythicModifyStringDialog } from '../../MythicComponents/MythicDialog';
import Paper from '@mui/material/Paper';
import {useTheme} from '@mui/material/styles';
import Grow from '@mui/material/Grow';
import Popper from '@mui/material/Popper';
import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import VisibilityIcon from '@mui/icons-material/Visibility';
import Divider from '@mui/material/Divider';
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';
@@ -27,8 +17,15 @@ import SettingsIcon from '@mui/icons-material/Settings';
import EditIcon from '@mui/icons-material/Edit';
import AccountTreeIcon from '@mui/icons-material/AccountTree';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import ListSubheader from '@mui/material/ListSubheader';
import {uniqueSplitString} from './CallbacksTabsProcessBrowser';
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!) {
@@ -46,42 +43,54 @@ const updateFileComment = gql`
}
}
`;
export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, me, onRowDoubleClick, onTaskRowAction, host, group, showDeletedFiles, tabInfo}) => {
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", "Tags", "Comment"],
"hidden": [ "Session" ]
"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 = () => {
const openAllNodes = (state) => {
let onodes = {};
for(const [group, hosts] of Object.entries(updatedTreeAdjMatrix)){
for(const [host, matrix] of Object.entries(hosts)){
for(const [key, children] of Object.entries(matrix)){
onodes[key] = true;
//if(treeRootData[host][key] !== undefined){
//onodes[treeRootData[host][key].id] = true;
//onodes[parseInt(key)] = true;
//}
onodes[key] = state;
}
}
}
setOpenNodes(onodes);
}
React.useEffect( () => {
// need to update the matrix in case there are notes that don't trace back to root
// need to update the matrix in case there are nodes that don't trace back to root
let adjustedMatrix = {};
// check for cycles
let tempMatrix = {...treeAdjMatrix};
@@ -117,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][""] = {}}
@@ -127,18 +139,55 @@ 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();
}, [host, group])
React.useEffect(() => {
openAllNodes(true);
setViewSingleTreeData(false);
}, [host]);
}, [host, group])
const onExpandNode = (nodeId) => {
setOpenNodes({
...openNodes,
@@ -162,20 +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: '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}];
@@ -186,7 +225,7 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
return [...prev];
}
}, [])
, [filterOptions, columnVisibility]
, [filterOptions, columnVisibility, columnOrder]
);
const flattenNode = useCallback(
(node, host, group, depth = 0) => {
@@ -353,15 +392,24 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
}
return tempData;
}, [allData, sortData]);
const onSubmitFilterOptions = (newFilterOptions) => {
setFilterOptions(newFilterOptions);
openAllNodes();
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
}
openAllNodes(true);
}
const filterRow = (rowData) => {
if(rowData.root){return true}
if(!showDeletedFiles &&
treeRootData[group][host][rowData.full_path_text /*+ uniqueSplitString + rowData.callback_id*/] !== undefined &&
treeRootData[group][host][rowData.full_path_text /*+ uniqueSplitString + rowData.callback_id*/].deleted){
treeRootData[group][host][rowData.full_path_text] !== undefined &&
treeRootData[group][host][rowData.full_path_text ].deleted){
return true;
}
let filterOptionInMetadata = {}
@@ -373,13 +421,13 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
}
}
for(const [key,value] of Object.entries(filterOptions)){
if(treeRootData[group][host][rowData.full_path_text /*+ uniqueSplitString + rowData.callback_id*/] === undefined){return true}
if(treeRootData[group][host][rowData.full_path_text] === undefined){return true}
if(filterOptionInMetadata[key]){
if(!String(treeRootData[group][host][rowData.full_path_text /*+ uniqueSplitString + rowData.callback_id*/]?.metadata[key]).toLowerCase().includes(value)){
if(!String(treeRootData[group][host][rowData.full_path_text ]?.metadata[key]).toLowerCase().includes(value)){
return true;
}
}else{
if(!String(treeRootData[group][host][rowData.full_path_text /*+ uniqueSplitString + rowData.callback_id*/][key]).toLowerCase().includes(value)){
if(!String(treeRootData[group][host][rowData.full_path_text][key]).toLowerCase().includes(value)){
return true;
}
}
@@ -448,7 +496,7 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
}
setSingleTreeData(singleTreeAdjMatrix);
onSubmitFilterOptions({});
onSubmitFilterOptions(undefined);
}
const toggleViewSingleTreeData = () => {
setViewSingleTreeData(!viewSingleTreeData);
@@ -471,13 +519,14 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
viewSingleTreeData={viewSingleTreeData}
setSingleTree={setSingleTree}
toggleViewSingleTreeData={toggleViewSingleTreeData}
getLoadedCommandForUIFeature={getLoadedCommandForUIFeature}
onTaskRowAction={onTaskRowAction} />;
case "Name":
return <FileBrowserTableRowNameCell
treeRootData={treeRootData[group]}
host={host}
group={group}
children={updatedTreeAdjMatrix[group][host]?.[row.full_path_text /*+ uniqueSplitString + row.callback_id*/]}
children={updatedTreeAdjMatrix[group][host]?.[row.full_path_text ]}
handleOnClickButton={handleOnClickButton}
rowData={row} />;
case "User":
@@ -485,21 +534,21 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
treeRootData={treeRootData[group]}
host={host}
group={group}
cellData={treeRootData[group][host][row.full_path_text /*+ uniqueSplitString + row.callback_id*/]?.metadata?.user || ''}
cellData={treeRootData[group][host][row.full_path_text]?.metadata?.user || ''}
rowData={row} />;
case "Arch":
return <FileBrowserTableRowStringCell
treeRootData={treeRootData[group]}
host={host}
group={group}
cellData={treeRootData[group][host][row.full_path_text /*+ uniqueSplitString + row.callback_id*/]?.metadata?.architecture || ''}
cellData={treeRootData[group][host][row.full_path_text]?.metadata?.architecture || ''}
rowData={row} />;
case "Session":
return <FileBrowserTableRowStringCell
treeRootData={treeRootData[group]}
host={host}
group={group}
cellData={treeRootData[group][host][row.full_path_text /*+ uniqueSplitString + row.callback_id*/]?.metadata?.session_id || ''}
cellData={treeRootData[group][host][row.full_path_text ]?.metadata?.session_id || ''}
rowData={row} />;
case "PID":
return <FileBrowserTableRowStringCell
@@ -507,14 +556,21 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
host={host}
group={group}
rowData={row}
cellData={treeRootData[group][host][row.full_path_text /*+ uniqueSplitString + row.callback_id*/]?.metadata?.process_id || row.full_path_text} />;
cellData={row.full_path_text} />;
case "PPID":
return <FileBrowserTableRowStringCell
treeRootData={treeRootData[group]}
host={host}
group={group}
rowData={row}
cellData={treeRootData[group][host][row.full_path_text /*+ uniqueSplitString + row.callback_id*/]?.metadata?.parent_process_id || ""} />;
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}
@@ -528,15 +584,23 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
host={host}
rowData={row}
group={group}
cellData={treeRootData[group][host][row.full_path_text /*+ uniqueSplitString + row.callback_id*/]?.comment}
cellData={treeRootData[group][host][row.full_path_text]?.comment}
/>;
case "CMD":
return <FileBrowserTableRowStringCell
treeRootData={treeRootData[group]}
host={host}
rowData={row}
group={group}
cellData={treeRootData[group][host][row.full_path_text ]?.metadata?.command_line || ""}
/>;
default:
console.log("hit default case in swith on c.name)")
console.log("hit default case in switch on c.name)", c.name)
}
})];
}
}, []),
[sortedData, onTaskRowAction, filterOptions, columnVisibility, showDeletedFiles]
[sortedData, onTaskRowAction, filterOptions, columnVisibility, showDeletedFiles, treeRootData]
);
const onClickHeader = (e, columnIndex) => {
const column = columns[columnIndex];
@@ -557,13 +621,17 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
}
};
const localOnDoubleClick = (e, rowIndex) => {
const rowData = treeRootData[group][host][allData[rowIndex]];
const rowData = treeRootData[group][host][sortedData[rowIndex]["full_path_text"]];
onRowDoubleClick(rowData);
};
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;
@@ -573,62 +641,165 @@ 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( () => {
if(viewSingleTreeData){
return;
}
if(expandOrCollapseAll){
openAllNodes(true);
} else {
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" }}>
<div style={{ width: '100%', height: '100%', overflow: "hidden", position: "relative" }}>
<MythicResizableGrid
columns={columns}
sortIndicatorIndex={sortColumn}
sortDirection={sortData.sortDirection}
items={gridData}
rowHeight={35}
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>
)
}
const FileBrowserTableRowNameCell = ({ rowData, treeRootData, host, children, handleOnClickButton }) => {
const theme = useTheme();
return (
<div style={{ alignItems: 'center', display: 'flex', flexGrow: 1, width: "100%", textDecoration: treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/]?.deleted ? 'line-through' : '' }}>
<div style={{ display: "inline-flex", height: "100%", alignItems: "center", width: "100%", textDecoration: treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/]?.deleted ? 'line-through' : '' }}>
{[...Array(rowData.depth-1)].map((o, i) => (
i === rowData.depth-2 && children ? (
i === 0 ? (<div key={'folder' + rowData.full_path_text + i} style={{marginLeft: 10, paddingRight: 10}}></div>) : null
i === 0 ? (<div key={'folder' + rowData.full_path_text + i} style={{marginLeft: 10, paddingRight: 10, height: "100%"}}></div>) : null
) : (
<div
key={'folder' + rowData.full_path_text + 'lines' + i}
@@ -644,7 +815,7 @@ const FileBrowserTableRowNameCell = ({ rowData, treeRootData, host, children, ha
))}
{children === undefined ? (
<>
<div style={{display:"inline-block", width: rowData.depth === 1 ? "1.2rem" : ""}}></div>
<div style={{display:"inline-block", height: "100%", width: rowData.depth === 1 ? "1.2rem" : ""}}></div>
<TerminalIcon />
</>
) : rowData.isOpen ? (
@@ -659,7 +830,7 @@ const FileBrowserTableRowNameCell = ({ rowData, treeRootData, host, children, ha
) : (
<>
<KeyboardArrowRightIcon
style={{ paddingTop: '5px', }}
style={{ }}
onClick={() => {handleOnClickButton(rowData.full_path_text /*+ uniqueSplitString + rowData["callback_id"]*/)}} />
<TerminalIcon />
</>
@@ -689,19 +860,29 @@ 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({});
const [fileCommentDialogOpen, setFileCommentDialogOpen] = React.useState(false);
const [getPermissions] = useLazyQuery(getPermissionsDataQuery, {
onCompleted: (data) => {
console.log(rowData);
setPermissionData({...data.mythictree_by_pk.metadata,
callback_id: rowData["callback_id"],
callback_display_id: rowData["callback_display_id"]});
callback_display_id: rowData["callback_display_id"],
callbacks: rowData['callbacks']});
setViewPermissionsDialogOpen(true);
},
onError: (data) => {
@@ -714,24 +895,13 @@ 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 = (whichOption, event, index, callback_id, callback_display_id) => {
switch (whichOption){
case "A":
optionsA[index].click(event, callback_id, callback_display_id);
break;
case "B":
optionsB[index].click(event, callback_id, callback_display_id);
break;
default:
break;
}
const handleMenuItemClick = (event, click) => {
click({event});
setDropdownOpen(false);
};
const handleClose = (event) => {
@@ -739,18 +909,22 @@ const FileBrowserTableRowActionCell = ({rowData, onTaskRowAction, treeRootData,
return;
}
setDropdownOpen(false);
setCustomMenuOptions([loadingMenuDisplay]);
};
const optionsA = [
{
name: 'View Detailed Data', icon: <VisibilityIcon style={{paddingRight: "5px"}}/>, click: (evt) => {
evt.stopPropagation();
getPermissions({variables: {mythictree_id: rowData.id}});
}},
name: 'View Detailed Data', icon: <VisibilityIcon style={{paddingRight: "5px"}}/>,
type: "item", disabled: false,
click: ({event}) => {
event.stopPropagation();
getPermissions({variables: {mythictree_id: rowData.id}});
}
},
{
name: 'Edit Comment',
name: 'Edit Comment', type: "item", disabled: false,
icon: <EditIcon style={{ paddingRight: '5px' }} />,
click: (evt) => {
evt.stopPropagation();
click: ({event}) => {
event.stopPropagation();
setFileCommentDialogOpen(true);
},
},
@@ -759,54 +933,135 @@ const FileBrowserTableRowActionCell = ({rowData, onTaskRowAction, treeRootData,
icon: viewSingleTreeData ?
<VisibilityOffIcon style={{paddingRight: "5px", color: theme.palette.error.main}}/> :
<AccountTreeIcon style={{paddingRight: "5px", color: theme.palette.success.main}}/>,
click: (evt) => {
type: "item",
disabled: false,
click: ({event}) => {
setSingleTree(rowData);
toggleViewSingleTreeData();
}
}
];
const optionsB = [
{name: 'Task Inject', icon: <GetAppIcon style={{paddingRight: "5px", color: theme.palette.success.main}}/>, click: (evt, callback_id, display_id) => {
evt.stopPropagation();
onTaskRowAction({
process_id: treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/].metadata.process_id,
architecture: treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/].metadata.architecture,
uifeature: "process_browser:inject", callback_id, display_id
});
}},
{name: 'Task Token Listing', icon: <ListIcon style={{paddingRight: "5px", color: theme.palette.warning.main}}/>, click: (evt, callback_id, display_id) => {
evt.stopPropagation();
onTaskRowAction({
process_id: treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/].metadata.process_id,
architecture: treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/].metadata.architecture,
uifeature: "process_browser:list_tokens", callback_id, display_id
});
}, os: ["windows"]},
{name: 'Task Steal Token', icon: <DeleteIcon style={{paddingRight: "5px", color: theme.palette.error.main}}/>, click: (evt, callback_id, display_id) => {
evt.stopPropagation();
onTaskRowAction({
process_id: treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/].metadata.process_id,
architecture: treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/].metadata.architecture,
uifeature: "process_browser:steal_token", callback_id, display_id
});
}, os: ["windows"]},
{name: 'Task Kill Process', icon: <DeleteIcon style={{paddingRight: "5px", color: theme.palette.error.main}}/>, click: (evt, callback_id, display_id) => {
evt.stopPropagation();
onTaskRowAction({
process_id: treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/].metadata.process_id,
architecture: treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/].metadata.architecture,
uifeature: "process_browser:kill",
confirm_dialog: true, callback_id, 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
});
}
},
{
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
});
}
},
];
}
async function getMenuOptions() {
let options = [...optionsA];
options.push(...(await optionsB(tabInfo["callbackID"], tabInfo["displayID"])));
if(treeRootData[host][rowData["full_path_text"] ]?.callback?.["id"] !== tabInfo["callbackID"]){
options.push({
name: `Original Callback: ${treeRootData[host][rowData["full_path_text"] ]?.callback?.["id"]}`,
icon: null, click: () => {}, type: "menu",
menuItems: [
...(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"] /*+ uniqueSplitString + rowData["callback_id"]*/]?.id ? (
treeRootData[host][rowData["full_path_text"] ]?.id ? (
<React.Fragment>
<IconButton
size="small"
style={{height: "100%" }}
aria-controls={dropdownOpen ? 'split-button-menu' : undefined}
aria-expanded={dropdownOpen ? 'true' : undefined}
aria-haspopup="menu"
@@ -817,74 +1072,43 @@ const FileBrowserTableRowActionCell = ({rowData, onTaskRowAction, treeRootData,
>
<SettingsIcon />
</IconButton>
<Popper open={dropdownOpen} anchorEl={dropdownAnchorRef.current} role={undefined} transition style={{zIndex: 4}}
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
}}
>
<Paper className={"dropdownMenuColored"}>
<ClickAwayListener onClickAway={handleClose}>
<MenuList id="split-button-menu">
{optionsA.map((option, index) => (
option.os === undefined || option.os.includes(treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/].os) ? (
<MenuItem
key={option.name}
onClick={(event) => handleMenuItemClick("A", event, index)}
>
{option.icon}{option.name}
</MenuItem>
) : null
))}
<Divider />
<ListSubheader component={"li"} className={"MuiListSubheader-root"}>
Act from current Callback: {tabInfo["displayID"]}
</ListSubheader>
{optionsB.map((option, index) => (
option.os === undefined || option.os.includes(treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/].os) ? (
<MenuItem
key={option.name}
onClick={(event) => handleMenuItemClick("B", event,
index,
tabInfo["callbackID"],
tabInfo["displayID"])}
>
{option.icon}{option.name}
</MenuItem>
) : null
))}
{
treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/]?.callback && treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/]?.callback?.["id"] !== tabInfo["callbackID"] &&
<ListSubheader component={"li"} className={"MuiListSubheader-root"}>
Act from originating Callback: {treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/]?.callback?.["display_id"] || tabInfo["displayID"]}
</ListSubheader>
}{
treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/]?.callback && treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/]?.callback?.["id"] !== tabInfo["callbackID"] &&
optionsB.map((option, index) => (
option.os === undefined || option.os.includes(treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/].os) ? (
<MenuItem
{dropdownOpen &&
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
<Dropdown
isOpen={dropdownAnchorRef.current}
onOpen={setDropdownOpen}
externallyOpen={dropdownOpen}
anchorReference={"anchorEl"}
menu={[
...customMenuOptions.map((option, index) => (
option.type === 'item' ? (
<DropdownMenuItem
key={option.name}
onClick={(event) => handleMenuItemClick('B', event,
index,
treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/]?.["callback"]?.["id"] || tabInfo["callbackID"],
treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/]?.["callback"]?.["display_id"] || tabInfo["displayID"])}>
{option.icon}
{option.name}
</MenuItem>
): null
))
}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
disabled={option.disabled}
onClick={(event) => handleMenuItemClick(event, option.click)}
>
{option.icon} {option.name}
</DropdownMenuItem>
) : option.type === 'menu' ? (
<DropdownNestedMenuItem
label={option.name}
disabled={option.disabled}
menu={
option.menuItems.map((menuOption, indx) => (
<DropdownMenuItem
key={menuOption.name}
disabled={menuOption.disabled}
onClick={(event) => handleMenuItemClick(event, menuOption.click)}
>
{menuOption.icon}{menuOption.name}
</DropdownMenuItem>
))
}
/>
) : null))
]}/>
</ClickAwayListener>
}
{fileCommentDialogOpen && (
<MythicDialog
fullWidth={true}
@@ -906,7 +1130,7 @@ const FileBrowserTableRowActionCell = ({rowData, onTaskRowAction, treeRootData,
/>
)}
{viewPermissionsDialogOpen &&
<MythicDialog fullWidth={true} maxWidth="md" open={viewPermissionsDialogOpen}
<MythicDialog fullWidth={true} maxWidth="xl" open={viewPermissionsDialogOpen}
onClose={()=>{setViewPermissionsDialogOpen(false);}}
innerDialog={<MythicViewJSONAsTableDialog title="View Detailed Data" leftColumn="Attribute"
rightColumn="Value" value={permissionData}
@@ -9,6 +9,8 @@ import {CallbacksTabsTaskingInput} from "./CallbacksTabsTaskingInput";
import {CallbacksTableIPCell, CallbacksTableLastCheckinCell} from "./CallbacksTableRow";
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`
@@ -24,7 +26,13 @@ query callbacksAndFeatures($payloadtype_id: Int!) {
display_id
last_checkin
ip
dead
mythictree_groups_string
payload {
payloadtype {
agent_type
}
}
}
}`;
@@ -54,14 +62,14 @@ const columns = [
{
field: 'ip',
headerName: 'IP',
flex: 1,
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;
}
}
},
@@ -69,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} />,
},
@@ -81,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]);
@@ -119,7 +130,7 @@ const CustomSelectTable = ({initialData, selectedData}) => {
)
}
export function CallbacksTabsTaskMultipleDialog({onClose, callback, me}) {
export function CallbacksTabsTaskMultipleDialog({onClose, callback}) {
const mountedRef = React.useRef(true);
const [selectedToken, setSelectedToken] = React.useState({});
const [openTaskingButton, setOpenTaskingButton] = React.useState(false);
@@ -139,8 +150,8 @@ export function CallbacksTabsTaskMultipleDialog({onClose, callback, me}) {
const submitTasking = () => {
//console.log("selectedFeature", selectedFeature)
if(selectedData.current.length === 0){
onClose();
console.log("selectedData.current.length === 0")
//onClose();
snackActions.warning("No callbacks selected");
return;
}
const callbacks = selectedData.current.map( c => c.display_id)
@@ -163,6 +174,11 @@ export function CallbacksTabsTaskMultipleDialog({onClose, callback, me}) {
}
const onSubmitCommandLine = (message, cmd, parsed, force_parsed_popup, cmdGroupNames, previousTaskingLocation) => {
//console.log(message, cmd, parsed);
if(selectedData.current.length === 0){
//onClose();
snackActions.warning("No callbacks selected");
return;
}
let params = message.split(" ");
delete params[0];
params = params.join(" ").trim();
@@ -181,7 +197,8 @@ export function CallbacksTabsTaskMultipleDialog({onClose, callback, me}) {
openDialog: false,
parameters: params,
tasking_location: newTaskingLocation,
dontShowSuccessDialog: false
dontShowSuccessDialog: false,
payload_type: cmd.payloadtype?.name,
};
submitTasking();
return;
@@ -226,7 +243,8 @@ export function CallbacksTabsTaskMultipleDialog({onClose, callback, me}) {
groupName: cmdGroupNames[0],
parameters: params,
tasking_location: newTaskingLocation,
dontShowSuccessDialog: false
dontShowSuccessDialog: false,
payload_type: cmd.payloadtype?.name,
};
}else{
finalTaskedParameters.current = undefined;
@@ -237,7 +255,8 @@ export function CallbacksTabsTaskMultipleDialog({onClose, callback, me}) {
parsedParameters: parsed,
parameters: params,
tasking_location: newTaskingLocation,
dontShowSuccessDialog: false
dontShowSuccessDialog: false,
payload_type: cmd.payloadtype?.name,
};
}
submitTasking();
@@ -254,7 +273,8 @@ export function CallbacksTabsTaskMultipleDialog({onClose, callback, me}) {
parsedParameters: parsed,
tasking_location: newTaskingLocation,
dontShowSuccessDialog: false,
parameter_group_name: cmdGroupNames[0]
parameter_group_name: cmdGroupNames[0],
payload_type: cmd.payloadtype?.name,
};
submitTasking();
}
@@ -270,33 +290,37 @@ export function CallbacksTabsTaskMultipleDialog({onClose, callback, me}) {
}
}
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}
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,21 +47,21 @@ 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={
<MythicModifyStringDialog title={"Edit Tab's Description"} onClose={() => {setOpenEditDescriptionDialog(false);}} value={description} onSubmit={editDescriptionSubmit} />
<MythicModifyStringDialog title={"Edit Tab's Description - Displays as one line"} onClose={() => {setOpenEditDescriptionDialog(false);}} value={description} onSubmit={editDescriptionSubmit} />
}
/>
}
</React.Fragment>
</>
)
}
// this is to listen for the latest tasking
const fetchLimit = 10;
const fetchLimit = 30;
const getTaskingQuery = gql`
${taskingDataFragment}
subscription getTasking($callback_id: Int!, $fromNow: timestamp!, $limit: Int){
@@ -81,13 +82,14 @@ query getBatchTasking($callback_id: Int!, $offset: Int!, $fetchLimit: Int!){
}
}
`;
export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, parentMountedRef, me}) =>{
const [taskLimit, setTaskLimit] = React.useState(10);
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": [],
@@ -173,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) => {
@@ -204,23 +209,11 @@ export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, pa
}
useSubscription(getTaskingQuery, {
variables: {callback_id: tabInfo.callbackID, fromNow:fromNow, limit: taskLimit},
shouldResubscribe: true,
onError: data => {
console.error(data)
},
fetchPolicy: "no-cache",
onData: subscriptionDataCallback});
const scrollToBottom = useCallback( () => {
if(taskingData && messagesEndRef.current){
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
}
}, [taskingData, messagesEndRef]);
useLayoutEffect( () => {
if(canScroll){
scrollToBottom();
setCanScroll(false);
}
}, [canScroll, scrollToBottom]);
const [getInfiniteScrollTasking, {loading: loadingMore}] = useLazyQuery(getNextBatchTaskingQuery, {
onError: data => {
console.error(data);
@@ -259,6 +252,7 @@ export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, pa
setFetchedAllTasks(false);
}
}
if(!scrollToBottom){setScrollToBottom(true)}
},
fetchPolicy: "no-cache"
});
@@ -270,11 +264,16 @@ export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, pa
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect( () => {
if(scrollToBottom){
messagesEndRef.current.scrollIntoView();
}
}, [scrollToBottom]);
const loadMoreTasks = () => {
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();
@@ -286,7 +285,13 @@ export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, pa
}
if(cmd.commandparameters.length === 0){
// if there are no parameters, just send whatever the user types along
onCreateTask({callback_id: tabInfo.displayID, command: cmd.cmd, params: params, parameter_group_name: "Default", tasking_location: newTaskingLocation});
onCreateTask({callback_id: tabInfo.displayID,
command: cmd.cmd,
params: params,
parameter_group_name: "Default",
tasking_location: newTaskingLocation,
payload_type: cmd.payloadtype?.name,
});
}else{
// check if there's a "file" component that needs to be displayed
const fileParamExists = cmd.commandparameters.find(param => {
@@ -333,20 +338,28 @@ export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, pa
params: JSON.stringify(parsed),
tasking_location: newTaskingLocation,
original_params: params,
parameter_group_name: cmdGroupNames[0]});
parameter_group_name: cmdGroupNames[0],
payload_type: cmd.payloadtype?.name,
});
}
}
}
const submitParametersDialog = (cmd, parameters, files, selectedParameterGroup) => {
const submitParametersDialog = (cmd, parameters, files, selectedParameterGroup, payload_type) => {
setOpenParametersDialog(false);
onCreateTask({callback_id: tabInfo.displayID, command: cmd, params: parameters, files: files, tasking_location: "modal", parameter_group_name: selectedParameterGroup});
onCreateTask({callback_id: tabInfo.displayID,
command: cmd,
params: parameters,
files: files,
tasking_location: "modal",
parameter_group_name: selectedParameterGroup,
payload_type: payload_type
});
}
const onCreateTask = ({callback_id, command, params, files, tasking_location, original_params, parameter_group_name}) => {
//console.log(selectedToken)
const onCreateTask = ({callback_id, command, params, files, tasking_location, original_params, parameter_group_name, payload_type}) => {
if(selectedToken.token_id !== undefined){
createTask({variables: {callback_id, command, params, files, token_id: selectedToken.token_id, tasking_location, original_params, parameter_group_name}});
createTask({variables: {callback_id, command, params, files, token_id: selectedToken.token_id, tasking_location, original_params, parameter_group_name, payload_type}});
}else{
createTask({variables: {callback_id, command, params, files, tasking_location, original_params, parameter_group_name}});
createTask({variables: {callback_id, command, params, files, tasking_location, original_params, parameter_group_name, payload_type}});
}
}
const onSubmitFilter = (newFilter) => {
@@ -361,30 +374,45 @@ 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} >
{!fetchedAllTasks &&
<MythicStyledTooltip title="Fetch Older Tasks">
<IconButton
onClick={loadMoreTasks}
variant="contained"
color="primary"
style={{marginLeft: "50%"}}
size="large"><AutorenewIcon /></IconButton>
</MythicStyledTooltip>}
{!fetched && <LinearProgress color="primary" thickness={2} style={{paddingTop: "5px"}}/>}
{loadingMore && <LinearProgress color="primary" thickness={2} style={{paddingTop: "5px"}}/>}
<div style={{overflowY: "auto", flexGrow: 1}}>
{
taskingData.task.map( (task) => (
<TaskDisplay key={"taskinteractdisplay" + task.id} me={me} task={task} command_id={task.command == null ? 0 : task.command.id}
filterOptions={filterOptions} newlyIssuedTasks={newlyIssuedTasks.current}/>
))
}
<div style={{overflowY: "auto", flexGrow: 1, width: "100%"}} id={`taskingPanel${tabInfo.callbackID}`}>
{!fetchedAllTasks &&
<MythicStyledTooltip tooltipStyle={{marginLeft: "50%"}} title="Fetch Older Tasks">
<IconButton
onClick={loadMoreTasks}
variant="contained"
color="success"
size="large"><AutorenewIcon/></IconButton>
</MythicStyledTooltip>}
{
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}/>
))
}
<div ref={messagesEndRef}/>
</div>
<div ref={messagesEndRef} />
<CallbacksTabsTaskingInput filterTasks={true} me={me} onSubmitFilter={onSubmitFilter} onSubmitCommandLine={onSubmitCommandLine} changeSelectedToken={changeSelectedToken}
filterOptions={filterOptions} callback_id={tabInfo.callbackID} callback_os={tabInfo.os} parentMountedRef={mountedRef} />
<CallbacksTabsTaskingInput filterTasks={true} me={me}
onSubmitFilter={onSubmitFilter}
onSubmitCommandLine={onSubmitCommandLine}
changeSelectedToken={changeSelectedToken}
filterOptions={filterOptions}
focus={index === value}
callback_id={tabInfo.callbackID}
payloadtype_name={tabInfo.payloadtype}
callback_os={tabInfo.os} parentMountedRef={mountedRef} />
{openParametersDialog &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openParametersDialog}
onClose={()=>{setOpenParametersDialog(false);}}
@@ -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,11 +48,11 @@ 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={
<MythicModifyStringDialog title={"Edit Tab's Description"} onClose={() => {setOpenEditDescriptionDialog(false);}} value={description} onSubmit={editDescriptionSubmit} />
<MythicModifyStringDialog title={"Edit Tab's Description - Displays as one line"} onClose={() => {setOpenEditDescriptionDialog(false);}} value={description} onSubmit={editDescriptionSubmit} />
}
/>
}
@@ -60,7 +61,7 @@ export function CallbacksTabsTaskingConsoleLabel(props){
}
// this is to listen for the latest tasking
const fetchLimit = 10;
const fetchLimit = 20;
const getTaskingQuery = gql`
${taskingDataFragment}
subscription getTasking($callback_id: Int!, $fromNow: timestamp!, $limit: Int){
@@ -81,13 +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": [],
@@ -173,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) => {
@@ -210,17 +215,6 @@ export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onClose
},
fetchPolicy: "no-cache",
onData: subscriptionDataCallback});
const scrollToBottom = useCallback( () => {
if(taskingData && messagesEndRef.current){
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
}
}, [taskingData, messagesEndRef]);
useLayoutEffect( () => {
if(canScroll){
scrollToBottom();
setCanScroll(false);
}
}, [canScroll, scrollToBottom]);
const [getInfiniteScrollTasking, {loading: loadingMore}] = useLazyQuery(getNextBatchTaskingQuery, {
onError: data => {
console.error(data);
@@ -259,6 +253,7 @@ export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onClose
setFetchedAllTasks(false);
}
}
if(!scrollToBottom){setScrollToBottom(true)}
},
fetchPolicy: "no-cache"
});
@@ -269,7 +264,12 @@ export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onClose
mountedRef.current = false;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
}, []);
useEffect( () => {
if(scrollToBottom){
messagesEndRef.current.scrollIntoView();
}
}, [scrollToBottom]);
const loadMoreTasks = () => {
getInfiniteScrollTasking({variables: {callback_id: tabInfo.callbackID, offset: taskingData.task.length, fetchLimit}});
}
@@ -286,7 +286,13 @@ export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onClose
}
if(cmd.commandparameters.length === 0){
// if there are no parameters, just send whatever the user types along
onCreateTask({callback_id: tabInfo.displayID, command: cmd.cmd, params: params, parameter_group_name: "Default", tasking_location: newTaskingLocation});
onCreateTask({callback_id: tabInfo.displayID,
command: cmd.cmd,
params: params,
parameter_group_name: "Default",
tasking_location: newTaskingLocation,
payload_type: cmd.payloadtype?.name,
});
}else{
// check if there's a "file" component that needs to be displayed
const fileParamExists = cmd.commandparameters.find(param => {
@@ -325,7 +331,7 @@ export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onClose
setCommandInfo({...cmd, "parsedParameters": parsed});
}
setOpenParametersDialog(true);
return;
}else{
delete parsed["_"];
onCreateTask({callback_id: tabInfo.displayID,
@@ -333,20 +339,28 @@ export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onClose
params: JSON.stringify(parsed),
tasking_location: newTaskingLocation,
original_params: params,
parameter_group_name: cmdGroupNames[0]});
parameter_group_name: cmdGroupNames[0],
payload_type: cmd.payloadtype?.name,
});
}
}
}
const submitParametersDialog = (cmd, parameters, files, selectedParameterGroup) => {
const submitParametersDialog = (cmd, parameters, files, selectedParameterGroup, payload_type) => {
setOpenParametersDialog(false);
onCreateTask({callback_id: tabInfo.displayID, command: cmd, params: parameters, files: files, tasking_location: "modal", parameter_group_name: selectedParameterGroup});
onCreateTask({callback_id: tabInfo.displayID,
command: cmd,
params: parameters,
files: files,
tasking_location: "modal",
parameter_group_name: selectedParameterGroup,
payload_type: payload_type
});
}
const onCreateTask = ({callback_id, command, params, files, tasking_location, original_params, parameter_group_name}) => {
//console.log(selectedToken)
const onCreateTask = ({callback_id, command, params, files, tasking_location, original_params, parameter_group_name, payload_type}) => {
if(selectedToken.token_id !== undefined){
createTask({variables: {callback_id, command, params, files, token_id: selectedToken.token_id, tasking_location, original_params, parameter_group_name}});
createTask({variables: {callback_id, command, params, files, token_id: selectedToken.token_id, tasking_location, original_params, parameter_group_name, payload_type}});
}else{
createTask({variables: {callback_id, command, params, files, tasking_location, original_params, parameter_group_name}});
createTask({variables: {callback_id, command, params, files, tasking_location, original_params, parameter_group_name, payload_type}});
}
}
const onSubmitFilter = (newFilter) => {
@@ -363,28 +377,33 @@ export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onClose
}
return (
<MythicTabPanel index={index} value={value} >
{!fetchedAllTasks &&
<MythicStyledTooltip title="Fetch Older Tasks">
<IconButton
onClick={loadMoreTasks}
variant="contained"
color="primary"
style={{marginLeft: "50%"}}
size="large"><AutorenewIcon /></IconButton>
</MythicStyledTooltip>}
{!fetched && <LinearProgress color="primary" thickness={2} style={{paddingTop: "5px"}}/>}
{loadingMore && <LinearProgress color="primary" thickness={2} style={{paddingTop: "5px"}}/>}
<div style={{overflowY: "auto", flexGrow: 1}}>
<div style={{overflowY: "auto", flexGrow: 1, width: "100%"}} id={`taskingPanelConsole${tabInfo.callbackID}`}>
{!fetchedAllTasks &&
<MythicStyledTooltip title="Fetch Older Tasks" style={{marginLeft: "50%"}}>
<IconButton
onClick={loadMoreTasks}
variant="contained"
color="success"
size="large"><AutorenewIcon /></IconButton>
</MythicStyledTooltip>}
{
taskingData.task.map( (task) => (
<TaskDisplayConsole key={"taskinteractdisplayconsole" + task.id} me={me} task={task} command_id={task.command == null ? 0 : task.command.id}
filterOptions={filterOptions} newlyIssuedTasks={newlyIssuedTasks.current}/>
taskingData.task.map((task) => (
<TaskDisplayConsole key={"taskinteractdisplayconsole" + task.id} me={me} task={task}
command_id={task.command == null ? 0 : task.command.id}
filterOptions={filterOptions} newlyIssuedTasks={newlyIssuedTasks.current}/>
))
}
<div ref={messagesEndRef}/>
</div>
<div ref={messagesEndRef} />
<CallbacksTabsTaskingInput filterTasks={true} me={me} onSubmitFilter={onSubmitFilter} onSubmitCommandLine={onSubmitCommandLine} changeSelectedToken={changeSelectedToken}
filterOptions={filterOptions} callback_id={tabInfo.callbackID} callback_os={tabInfo.os} parentMountedRef={mountedRef} />
filterOptions={filterOptions} callback_id={tabInfo.callbackID}
payloadtype_name={tabInfo.payloadtype} focus={index === value}
callback_os={tabInfo.os} parentMountedRef={mountedRef} />
{openParametersDialog &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openParametersDialog}
onClose={()=>{setOpenParametersDialog(false);}}
@@ -155,7 +155,7 @@ export function CallbacksTabsTaskingFilterDialog(props) {
}
return (
<Root>
<DialogTitle id="form-dialog-title">Filter Which Tasks Are Visible</DialogTitle>
<DialogTitle id="form-dialog-title">Filter Which Tasks Are Visible in this Callback</DialogTitle>
<DialogContent dividers={true} style={{overflow: "hidden"}}>
<React.Fragment>
<FormControl className={classes.formControl}>
File diff suppressed because it is too large. Load diff
@@ -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,11 +50,11 @@ 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={
<MythicModifyStringDialog title={"Edit Tab's Description"} onClose={() => {setOpenEditDescriptionDialog(false);}} value={description} onSubmit={editDescriptionSubmit} />
<MythicModifyStringDialog title={"Edit Tab's Description - Displays as one line"} onClose={() => {setOpenEditDescriptionDialog(false);}} value={description} onSubmit={editDescriptionSubmit} />
}
/>
}
@@ -63,7 +62,7 @@ export function CallbacksTabsTaskingSplitLabel(props){
)
}
// this is to listen for the latest tasking
const fetchLimit = 10;
const fetchLimit = 30;
const getTaskingQuery = gql`
${taskingDataFragment}
subscription getTasking($callback_id: Int!, $fromNow: timestamp!, $limit: Int){
@@ -83,13 +82,14 @@ query getBatchTasking($callback_id: Int!, $offset: Int!, $fetchLimit: Int!){
}
}
`;
export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTab, me}) =>{
const [taskLimit, setTaskLimit] = React.useState(10);
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": [],
@@ -99,7 +99,6 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
"everythingButList": [],
"hideErrors": false,
});
const [canScroll, setCanScroll] = React.useState(true);
const [selectedTask, setSelectedTask] = React.useState({id: 0});
const mountedRef = React.useRef(true);
const newlyIssuedTasks = useRef(null);
@@ -172,23 +171,20 @@ 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) => {
const index = prev.findIndex(element => element.id === cur.id);
if(index > -1){
// need to update an element
const updated = prev.map( (element) => {
if(element.id === cur.id){
if(selectedTask.id === cur.id){
return {...cur, selected: true}
}
return {...cur, selected: false};
}else{
return {...element, selected: false};
}
});
return updated;
prev[index] = {...cur}
if(prev[index].id === selectedTask.id){
prev[index].selected = true;
}
return [...prev];
}else{
return [...prev, cur];
}
@@ -198,8 +194,8 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
setTaskingData({task: mergedData});
}
if(mergedData.length > oldLength){
setCanScroll(true);
}
//setCanScroll(true);
}
if(mergedData.length > taskLimit){
setTaskLimit(mergedData.length);
}
@@ -218,17 +214,6 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
},
fetchPolicy: "no-cache",
onData: subscriptionDataCallback});
const scrollToBottom = useCallback( () => {
if(taskingData && messagesEndRef.current){
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
}
}, [taskingData, messagesEndRef]);
useLayoutEffect( () => {
if(canScroll){
scrollToBottom();
setCanScroll(false);
}
}, [canScroll, scrollToBottom]);
const [getInfiniteScrollTasking, {loading: loadingMore}] = useLazyQuery(getNextBatchTaskingQuery, {
onError: data => {
console.error(data);
@@ -270,17 +255,22 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
setFetchedAllTasks(false);
}
}
if(!scrollToBottom){setScrollToBottom(true)}
},
fetchPolicy: "no-cache"
});
useEffect( () => {
getInfiniteScrollTasking({variables: {callback_id: tabInfo.callbackID, offset: taskingData.task.length, fetchLimit}});
setCanScroll(true);
return() => {
mountedRef.current = false;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
}, []);
useEffect( () => {
if(scrollToBottom){
messagesEndRef.current.scrollIntoView();
}
}, [scrollToBottom]);
const loadMoreTasks = () => {
getInfiniteScrollTasking({variables: {callback_id: tabInfo.callbackID, offset: taskingData.task.length, fetchLimit}});
}
@@ -297,7 +287,13 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
}
if(cmd.commandparameters.length === 0){
// if there are no parameters, just send whatever the user types along
onCreateTask({callback_id: tabInfo.displayID, command: cmd.cmd, params: params, parameter_group_name: "Default", tasking_location: newTaskingLocation});
onCreateTask({callback_id: tabInfo.displayID,
command: cmd.cmd,
params: params,
parameter_group_name: "Default",
tasking_location: newTaskingLocation,
payload_type: cmd.payloadtype?.name,
});
}else{
// check if there's a "file" component that needs to be displayed
const fileParamExists = cmd.commandparameters.find(param => {
@@ -314,7 +310,8 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
}
}
});//console.log("missing File for group? ", fileParamExists, cmdGroupNames);
});
//console.log("missing File for group? ", fileParamExists, cmdGroupNames);
let missingRequiredPrams = false;
if(cmdGroupNames.length === 1){
const missingParams = cmd.commandparameters.filter(param => param.required && param.parameter_group_name === cmdGroupNames[0] && !(param.cli_name in parsed || param.name in parsed || param.display_name in parsed));
@@ -335,28 +332,36 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
setCommandInfo({...cmd, "parsedParameters": parsed});
}
setOpenParametersDialog(true);
return;
}else{
delete parsed["_"];
onCreateTask({callback_id: tabInfo.displayID,
command: cmd.cmd,
params: JSON.stringify(parsed),
tasking_location: newTaskingLocation,
original_params: params,
parameter_group_name: cmdGroupNames[0]});
}
onCreateTask({callback_id: tabInfo.displayID,
command: cmd.cmd,
params: JSON.stringify(parsed),
tasking_location: newTaskingLocation,
original_params: params,
parameter_group_name: cmdGroupNames[0],
payload_type: cmd.payloadtype?.name,
});
}
}
}
const submitParametersDialog = (cmd, parameters, files, selectedParameterGroup) => {
const submitParametersDialog = (cmd, parameters, files, selectedParameterGroup, payload_type) => {
setOpenParametersDialog(false);
onCreateTask({callback_id: tabInfo.displayID, command: cmd, params: parameters, files: files, tasking_location: "modal", parameter_group_name: selectedParameterGroup});
onCreateTask({callback_id: tabInfo.displayID,
command: cmd,
params: parameters,
files: files,
tasking_location: "modal",
parameter_group_name: selectedParameterGroup,
payload_type: payload_type
});
}
const onCreateTask = ({callback_id, command, params, files, tasking_location, original_params, parameter_group_name}) => {
//console.log(selectedToken)
const onCreateTask = ({callback_id, command, params, files, tasking_location, original_params, parameter_group_name, payload_type}) => {
if(selectedToken.token_id !== undefined){
createTask({variables: {callback_id, command, params, files, token_id: selectedToken.token_id, tasking_location, original_params, parameter_group_name}});
createTask({variables: {callback_id, command, params, files, token_id: selectedToken.token_id, tasking_location, original_params, parameter_group_name, payload_type}});
}else{
createTask({variables: {callback_id, command, params, files, tasking_location, original_params, parameter_group_name}});
createTask({variables: {callback_id, command, params, files, tasking_location, original_params, parameter_group_name, payload_type}});
}
}
const onSubmitFilter = (newFilter) => {
@@ -372,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;
@@ -397,49 +402,66 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
let index = taskingDataRef.current.task.findIndex(e => e.id === newlyIssuedTasks.current)
if(index > -1){
newlyIssuedTasks.current = null;
changeSelectedTask(taskingDataRef.current.task[index]);
if(taskingDataRef.current.task[index]?.operator?.username === (me?.user?.username || "")){
let el = document.getElementById(`taskingPanelSplit${tabInfo.callbackID}`);
if(el && el.scrollHeight - el.scrollTop - el.clientHeight < 100){
changeSelectedTask(taskingDataRef.current.task[index]);
}
}
}
}
}, [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]} >
<div className="bg-gray-base" style={{display: "inline-flex", flexDirection: "column"}}>
<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}}>
<div style={{overflowY: "auto", flexGrow: 1}} id={`taskingPanelSplit${tabInfo.callbackID}`}>
{!fetchedAllTasks &&
<MythicStyledTooltip title="Fetch Older Tasks">
<IconButton
onClick={loadMoreTasks}
variant="contained"
color="primary"
style={{marginLeft: "50%"}}
size="large"><AutorenewIcon /></IconButton>
<IconButton
onClick={loadMoreTasks}
variant="contained"
color="success"
style={{marginLeft: "50%"}}
size="large"><AutorenewIcon/></IconButton>
</MythicStyledTooltip>}
{!fetched && <LinearProgress color="primary" thickness={2} style={{paddingTop: "5px"}}/>}
{loadingMore && <LinearProgress color="info" thickness={2} style={{paddingTop: "5px"}}/>}
{
taskingData.task.map( (task) => (
<TaskDisplayFlat key={"taskinteractdisplaysplit" + task.id} me={me} task={task}
command_id={task.command == null ? 0 : task.command.id}
filterOptions={filterOptions}
onSelectTask={(tsk) => {changeSelectedTask(tsk)}}
showOnSelectTask={true} selectedTask={selectedTask}
/>
))
taskingData.task.map((task) => (
<TaskDisplayFlat key={"taskinteractdisplaysplit" + task.id} me={me} task={task}
command_id={task.command == null ? 0 : task.command.id}
filterOptions={filterOptions}
onSelectTask={(tsk) => {
changeSelectedTask(tsk)
}}
showOnSelectTask={true} selectedTask={selectedTask}
/>
))
}
<div ref={messagesEndRef}/>
</div>
<div ref={messagesEndRef} />
</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} focus={index === value}
onSubmitCommandLine={onSubmitCommandLine} changeSelectedToken={changeSelectedToken}
filterOptions={filterOptions} callback_id={tabInfo.callbackID}
callback_os={tabInfo.os} parentMountedRef={mountedRef} />
@@ -456,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,18 +1,23 @@
import React, {createContext} from 'react';
import {useSubscription, gql } 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( () => {});
export const CallbacksContext = createContext([]);
//
//callback(where: {active: {_eq: true}}){
const SUB_Callbacks = gql`
subscription CallbacksSubscription{
callback_stream(batch_size: 1000, cursor: {initial_value: {timestamp: "1970-01-01"}}) {
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
@@ -33,10 +38,28 @@ callback_stream(batch_size: 1000, cursor: {initial_value: {timestamp: "1970-01-0
user
agent_callback_id
operation_id
process_name
process_short_name
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 {
@@ -50,9 +73,69 @@ callback_stream(batch_size: 1000, cursor: {initial_value: {timestamp: "1970-01-0
}
}
`;
export const SUB_Edges = gql`
subscription CallbacksSubscription{
callbackgraphedge(order_by: {id: desc, end_timestamp: desc_nulls_first}) {
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
external_ip
host
id
integrity_level
ip
locked
locked_operator {
username
id
}
extra_info
sleep_info
pid
os
user
agent_callback_id
operation_id
process_short_name
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 {
name
agent_type
id
}
description
id
}
}
callbackgraphedge(order_by: {id: desc, end_timestamp: desc_nulls_first}) {
id
end_timestamp
destination {
@@ -68,6 +151,14 @@ subscription CallbacksSubscription{
process_name
integrity_level
extra_info
tags {
tagtype {
name
color
id
}
id
}
payload {
payloadtype {
name
@@ -77,6 +168,8 @@ subscription CallbacksSubscription{
callbackc2profiles {
c2profile {
name
has_logo
is_p2p
}
}
}
@@ -93,6 +186,14 @@ subscription CallbacksSubscription{
process_name
integrity_level
extra_info
tags {
tagtype {
name
color
id
}
id
}
payload {
payloadtype {
name
@@ -102,6 +203,8 @@ subscription CallbacksSubscription{
callbackc2profiles {
c2profile {
name
has_logo
is_p2p
}
}
}
@@ -109,96 +212,246 @@ subscription CallbacksSubscription{
id
is_p2p
name
has_logo
}
}
}
`;
export const SUB_Edges = gql`
subscription CallbackEdgeSubscription($fromNow: timestamp!){
callbackgraphedge_stream(batch_size: 100, cursor: {initial_value: {updated_at: $fromNow}}) {
id
end_timestamp
destination {
active
id
color
display_id
operation_id
user
host
ip
domain
os
process_name
integrity_level
extra_info
tags {
tagtype {
name
color
id
}
id
}
payload {
payloadtype {
name
id
}
}
callbackc2profiles {
c2profile {
name
has_logo
is_p2p
}
}
}
source {
active
id
color
display_id
operation_id
user
host
ip
domain
os
process_name
integrity_level
extra_info
tags {
tagtype {
name
color
id
}
id
}
payload {
payloadtype {
name
id
}
}
callbackc2profiles {
c2profile {
name
has_logo
is_p2p
}
}
}
c2profile {
id
is_p2p
name
has_logo
}
}
}
`;
export function CallbacksTop(props){
const me = props.me;
const [callbacks, setCallbacks] = React.useState([]);
const [callbackEdges, setCallbackEdges] = React.useState([]);
const callbacks = React.useRef([]);
const callbackEdges = React.useRef([]);
const fromNow = React.useRef(getSkewedNow());
const mountedRef = React.useRef(true);
const [loading, setLoading] = React.useState(true);
useSubscription(SUB_Callbacks, {
variables: {fromNow: fromNow.current},
fetchPolicy: "no-cache",
onData: ({data}) => {
if(!mountedRef.current){
return;
}
if(!mountedRef.current){
return;
}
const updated = data.data.callback_stream.reduce( (prev, cur) => {
let existingIndex = prev.findIndex( (element, i, array) => element.id === cur.id);
if(existingIndex === -1){
// cur isn't in our current list of callbacks
if(cur.active){return [...prev, cur]}
}
if(!cur.active){
if(existingIndex !== -1){
prev.splice(existingIndex, 1);
return [...prev];
}
const updated = data.data.callback_stream.reduce( (prev, cur) => {
console.log(cur)
let existingIndex = prev.findIndex( (element, i, array) => element.id === cur.id);
if(existingIndex === -1){
// cur isn't in our current list of callbacks
if(cur.active){return [...prev, cur]}
}
if(!cur.active){
if(existingIndex !== -1){
prev.splice(existingIndex, 1);
return [...prev];
}
}
prev[existingIndex] = {...prev[existingIndex], ...cur};
return [...prev];
}, callbacks);
updated.sort( (a, b) => a.display_id > b.display_id ? -1 : 1);
setCallbacks(updated);
}
prev[existingIndex] = {...prev[existingIndex], ...cur};
return [...prev];
}, callbacks.current);
updated.sort( (a, b) => a.display_id > b.display_id ? -1 : 1);
callbacks.current = updated;
},
onError: ({data}) => {
console.log(data)
},
});
useQuery(Query_Callbacks_And_Edges, {
fetchPolicy: "no-cache",
onCompleted: (data) => {
const updated = data.callback.reduce( (prev, cur) => {
let existingIndex = prev.findIndex( (element, i, array) => element.id === cur.id);
if(existingIndex === -1){
// cur isn't in our current list of callbacks
if(cur.active){return [...prev, cur]}
}
if(!cur.active){
if(existingIndex !== -1){
prev.splice(existingIndex, 1);
return [...prev];
}
}
prev[existingIndex] = {...prev[existingIndex], ...cur};
return [...prev];
}, callbacks.current);
updated.sort( (a, b) => a.display_id > b.display_id ? -1 : 1);
callbacks.current = updated;
callbackEdges.current = data.callbackgraphedge;
setLoading(false);
}
})
useSubscription(SUB_Edges, {
fetchPolicy: "network-only",
variables: {fromNow: fromNow.current},
onData: ({data}) => {
if(!mountedRef.current){
return;
}
setCallbackEdges(data.data.callbackgraphedge)
const updated = data.data.callbackgraphedge_stream.reduce( (prev, cur) => {
let existingIndex = prev.findIndex( (element, i, array) => element.id === cur.id);
if(existingIndex === -1){
// cur isn't in our current list of callbacks
return [...prev, cur]
}
prev[existingIndex] = {...prev[existingIndex], ...cur};
return [...prev];
}, callbackEdges.current);
callbackEdges.current = updated;
}
});
const onOpenTabLocal = React.useCallback( ({tabType, tabID, callbackID}) => {
for(let i = 0; i < callbacks.length; i++){
if(callbacks[i]["id"] === 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,
displayID: callbacks[i]["display_id"],
payloadtype: callbacks[i]["payload"]["payloadtype"]["name"],
payloadtype_id: callbacks[i]["payload"]["payloadtype"]["id"],
operation_id: callbacks[i]["operation_id"],
payloadDescription: callbacks[i]["payload"]["description"],
callbackDescription: callbacks[i]["description"],
host: callbacks[i]["host"],
os: callbacks[i]["payload"]["os"]};
displayID: callbacks.current[i]["display_id"],
payloadtype: callbacks.current[i]["payload"]["payloadtype"]["name"],
payloadtype_id: callbacks.current[i]["payload"]["payloadtype"]["id"],
operation_id: callbacks.current[i]["operation_id"],
payloadDescription: callbacks.current[i]["payload"]["description"],
callbackDescription: callbacks.current[i]["description"],
host: callbacks.current[i]["host"],
color: callbacks.current[i]["color"],
os: callbacks.current[i]["payload"]["os"],
customBrowser: customBrowser,
};
props.onOpenTab(tabData);
}
}
}, [callbacks, props.onOpenTab]);
}, [callbacks.current, props.onOpenTab]);
const onOpenTabsLocal = React.useCallback( ({tabs}) => {
let newTabData = [];
for(let j = 0; j < tabs.length; j++){
for(let i = 0; i < callbacks.current.length; i++){
if(callbacks.current[i]["id"] === tabs[j].callbackID){
const tabData = {
tabID: tabs[j].tabID,
tabType: tabs[j].tabType,
callbackID: tabs[j].callbackID,
displayID: callbacks.current[i]["display_id"],
payloadtype: callbacks.current[i]["payload"]["payloadtype"]["name"],
payloadtype_id: callbacks.current[i]["payload"]["payloadtype"]["id"],
operation_id: callbacks.current[i]["operation_id"],
payloadDescription: callbacks.current[i]["payload"]["description"],
callbackDescription: callbacks.current[i]["description"],
host: callbacks.current[i]["host"],
color: callbacks.current[i]["color"],
os: callbacks.current[i]["payload"]["os"],
customBrowser: tabs[j].customBrowser,
};
newTabData.push(tabData);
}
}
}
props.onOpenTabs(newTabData);
}, [callbacks.current, props.onOpenTabs]);
React.useEffect( () => {
return() => {
mountedRef.current = false;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const initialNewBrowserScriptTable = useMythicSetting({setting_name: "experiment-browserscripttable", default_value: "false"});
return (
<div style={{height: "100%", width: "100%"}}>
<CallbackGraphEdgesContext.Provider value={callbackEdges}>
<CallbackGraphEdgesContext.Provider value={callbackEdges.current}>
<OnOpenTabContext.Provider value={onOpenTabLocal}>
<CallbacksContext.Provider value={callbacks}>
{props.topDisplay === "graph" ? (
<CallbacksGraph maxHeight={"100%"} />
) : initialNewBrowserScriptTable ? (
<CallbacksTableMaterialReactTable onOpenTab={onOpenTabLocal}
parentMountedRef={mountedRef} me={me}/>
) : (
<CallbacksTable onOpenTab={onOpenTabLocal}
parentMountedRef={mountedRef} me={me}/>
)
}
</CallbacksContext.Provider>
<OnOpenTabsContext.Provider value={onOpenTabsLocal}>
<CallbacksContext.Provider value={callbacks.current}>
{props.topDisplay === "graph" ? (
<CallbacksGraph onOpenTab={onOpenTabLocal} maxHeight={"100%"} clickedTabId={props.clickedTabId} />
) : (
<CallbacksTable callbackTableGridRef={props.callbackTableGridRef}
parentMountedRef={mountedRef} me={me}
loading={loading}
clickedTabId={props.clickedTabId}/>
)
}
</CallbacksContext.Provider>
</OnOpenTabsContext.Provider>
</OnOpenTabContext.Provider>
</CallbackGraphEdgesContext.Provider>
</div>
@@ -12,7 +12,6 @@ import {useTheme} from '@mui/material/styles';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import Button from '@mui/material/Button';
import {ExpandedCallbackSideDetailsTable} from '../ExpandedCallback/ExpandedCallbackSideDetails';
import { toLocalTime } from '../../utilities/Time';
import {PayloadsTableRowBuildProcessPerStep} from '../Payloads/PayloadsTableRowBuildProgress';
@@ -24,17 +23,36 @@ import {AddRemoveCallbackCommandsDialog} from './AddRemoveCallbackCommandsDialog
import { snackActions } from '../../utilities/Snackbar';
import Box from '@mui/material/Box';
import Dialog from '@mui/material/Dialog';
import {ParseForDisplay} from "../Payloads/DetailedPayloadTable";
import {DetailedPayloadTable, ParseForDisplay} from "../Payloads/DetailedPayloadTable";
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
id
}
filemetum {
@@ -44,6 +62,23 @@ query GetCallbackDetails($callback_id: Int!) {
md5
sha1
}
operator {
username
}
eventstepinstance{
id
eventgroupinstance {
id
eventgroup {
id
name
}
}
eventstep {
id
name
}
}
payload_build_steps(order_by: {step_number: asc}) {
step_name
step_number
@@ -88,6 +123,9 @@ query GetCallbackDetails($callback_id: Int!) {
cmd
id
version
payloadtype {
name
}
}
}
architecture
@@ -118,6 +156,8 @@ query GetCallbackDetails($callback_id: Int!) {
process_name
init_callback
mythictree_groups
cwd
impersonation_context
}
}
@@ -145,6 +185,8 @@ 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([]);
const [buildParameters, setBuildParameters] = React.useState([]);
@@ -181,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) {
@@ -221,7 +264,7 @@ export function DetailedCallbackTable(props){
onCompleted: data => {
const commandState = data.callback_by_pk.loadedcommands.map( (c) =>
{
return {cmd: c.command.cmd, mythic: c.command.version, payload: c.version}
return {cmd: c.command.cmd, mythic: c.command.version, payload: c.version, payload_type: c.command.payloadtype.name}
}).sort((a,b) => (a.cmd > b.cmd) ? 1: ((b.cmd > a.cmd) ? -1 : 0));
setCommands(commandState);
const buildParametersState = data.callback_by_pk.payload.buildparameterinstances.map( (b) =>
@@ -257,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"}}/>;
}
@@ -300,7 +364,19 @@ export function DetailedCallbackTable(props){
</TableRow>
<TableRow hover>
<TableCell>UUID</TableCell>
<TableCell>{data.callback_by_pk.payload.uuid}</TableCell>
<TableCell>
{data.callback_by_pk.payload.uuid}
<IconButton disableFocusRipple={true}
disableRipple={true} size="small" color="info" onClick={() => setOpenDetailedView(true)}>
<InfoIconOutline />
</IconButton>
</TableCell>
{openDetailedView ? (
<MythicDialog fullWidth={true} maxWidth="lg" open={openDetailedView}
onClose={()=>{setOpenDetailedView(false);}}
innerDialog={<DetailedPayloadTable {...props} payload_id={data.callback_by_pk.payload.id} onClose={()=>{setOpenDetailedView(false);}} />}
/>
) : null }
</TableRow>
<TableRow hover>
<TableCell>Creation Time</TableCell>
@@ -314,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>
@@ -324,6 +415,40 @@ export function DetailedCallbackTable(props){
<TableCell>MD5</TableCell>
<TableCell>{data.callback_by_pk.payload.filemetum.md5}</TableCell>
</TableRow>
<TableRow hover>
<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>
<TableCell>Generated via Eventing</TableCell>
<TableCell>
<Link color="textPrimary" underline="always"
href={"/new/eventing?eventgroup=" +
data.callback_by_pk.payload?.eventstepinstance?.eventgroupinstance?.eventgroup.id +
"&eventgroupinstance=" +
data.callback_by_pk.payload?.eventstepinstance?.eventgroupinstance?.id
}>
{data.callback_by_pk.payload?.eventstepinstance?.eventgroupinstance?.eventgroup?.name} -
{data.callback_by_pk.payload?.eventstepinstance?.eventstep?.name}
</Link>
</TableCell>
</TableRow>
</>
}
</TableBody>
</Table>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
@@ -353,10 +478,10 @@ export function DetailedCallbackTable(props){
): (cmd.value)
)
}
{cmd.enc_key === null ? (null) : (<React.Fragment>
{cmd.enc_key === null ? null : (<React.Fragment>
<br/><b>Encryption Key: </b> {cmd.enc_key}
</React.Fragment>) }
{cmd.dec_key === null ? (null) : (<React.Fragment>
{cmd.dec_key === null ? null : (<React.Fragment>
<br/><b>Decryption Key: </b> {cmd.dec_key}
</React.Fragment>) }
</TableCell>
@@ -417,10 +542,10 @@ export function DetailedCallbackTable(props){
<TableCell>{cmd.description}</TableCell>
<TableCell>
<ParseForDisplay cmd={cmd} />
{cmd.enc_key === null ? (null) : (<React.Fragment>
{cmd.enc_key === null ? null : (<React.Fragment>
<br/><b>Encryption Key: </b> {cmd.enc_key}
</React.Fragment>) }
{cmd.dec_key === null ? (null) : (<React.Fragment>
{cmd.dec_key === null ? null : (<React.Fragment>
<br/><b>Decryption Key: </b> {cmd.dec_key}
</React.Fragment>) }
</TableCell>
@@ -441,6 +566,7 @@ export function DetailedCallbackTable(props){
<Table size="small" aria-label="details" style={{"overflowWrap": "break-word"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "40px"}}>Payload</TableCell>
<TableCell>Command Name</TableCell>
<TableCell>Mythic Version</TableCell>
<TableCell>Loaded Version</TableCell>
@@ -450,22 +576,27 @@ export function DetailedCallbackTable(props){
<TableBody>
{
commands.map( (cmd) => (
<TableRow key={cmd.cmd + data.callback_by_pk.payload.id} hover>
<TableCell>{cmd.cmd}</TableCell>
<TableRow key={cmd.cmd + cmd.payload_type} hover>
<TableCell>
<MythicAgentSVGIcon payload_type={cmd.payload_type} style={{width: "40px"}}/>
</TableCell>
<TableCell>{cmd.cmd} ({cmd.payload_type})</TableCell>
<TableCell>{cmd.mythic}</TableCell>
<TableCell>{cmd.payload}</TableCell>
<TableCell>
<Button variant="contained" color="primary" target="_blank"
href={"/docs/agents/" + data.callback_by_pk.payload.payloadtype.name + "/commands/" + cmd.cmd}>Docs</Button>
<IconButton variant="contained" target="_blank"
href={"/docs/agents/" + cmd.payload_type + "/commands/" + cmd.cmd}>
<MenuBookIcon />
</IconButton>
</TableCell>
</TableRow>
))
}
{openAddRemoveCommandsDialog &&
<MythicDialog fullWidth={true} maxWidth="md" open={openAddRemoveCommandsDialog}
onClose={()=>{setOpenAddRemoveCommandsDialog(false);}}
innerDialog={<AddRemoveCallbackCommandsDialog callback_id={props.callback_id}
innerDialog={<AddRemoveCallbackCommandsDialog
callback_id={props.callback_id}
display_id={props.display_id} onClose={()=>{setOpenAddRemoveCommandsDialog(false);}} onSubmit={addRemoveCommandsSubmit} />}
/>
}
@@ -1,23 +1,31 @@
import React, { useEffect } from 'react';
import Paper from '@mui/material/Paper';
import {useTheme} from '@mui/material/styles';
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 Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogTitle from '@mui/material/DialogTitle';
import GetAppIcon from '@mui/icons-material/GetApp';
import IconButton from '@mui/material/IconButton';
import {Typography, Link} from '@mui/material';
import {Button, Link, Typography} from '@mui/material';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {MythicDialog} from "../../MythicComponents/MythicDialog";
import {PreviewFileMediaDialog} from "../Search/PreviewFileMedia";
import {faPhotoVideo} from '@fortawesome/free-solid-svg-icons';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
export function DownloadHistoryDialog(props){
const [history, setHistory] = React.useState([]);
const theme = useTheme();
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);
}, [props.value]);
@@ -25,42 +33,70 @@ export function DownloadHistoryDialog(props){
<React.Fragment>
<DialogTitle id="form-dialog-title">{props.title}</DialogTitle>
<TableContainer component={Paper} className="mythicElement">
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
<TableContainer className="mythicElement">
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "5rem"}}>Download</TableCell>
<TableCell>Time</TableCell>
<TableCell>Task</TableCell>
<TableCell>Callback</TableCell>
<TableCell style={{}}></TableCell>
<TableCell style={{width: "15rem"}}>Time</TableCell>
<TableCell style={{width: "7rem"}}>Task</TableCell>
<TableCell style={{width: "7rem"}}>Callback</TableCell>
<TableCell>Comment</TableCell>
</TableRow>
</TableHead>
<TableBody>
{history.map( (hist) => (
<TableRow key={'hist' + hist.id}>
<TableCell >{hist.complete ? (
<IconButton
href={"/api/v1.4/files/download/" + hist.agent_file_id}
style={{color: theme.palette.success.main}}
size="large"><GetAppIcon /></IconButton>
) : (hist.chunks_received + "/" + hist.total_chunks)}</TableCell>
<TableCell >
{hist.complete ? (
<div style={{display: "inline-flex", alignItems: "center"}}>
<MythicStyledTooltip title={"Preview Media"}>
<FontAwesomeIcon icon={faPhotoVideo} style={{
height: "25px", marginRight: "5px", position: "relative", cursor: "pointer", display: "inline-block"}}
onClick={(e) => onPreviewMedia(e, hist)} />
</MythicStyledTooltip>
{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>
)
}
</div>
) : (hist.chunks_received + "/" + hist.total_chunks)}
</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>
@@ -26,6 +26,7 @@ import LayersIcon from '@mui/icons-material/Layers';
const getCallbackMythicTreeGroups = gql`
query getCallbackMythicTreeGroups($callback_id: Int!) {
callback_by_pk(id: $callback_id){
display_id
mythictree_groups
}
callback {
@@ -57,12 +58,14 @@ export function ModifyCallbackMythicTreeGroupsDialog(props){
onError: error => {
snackActions.error(error.message);
}
})
});
const callbackDisplayID = React.useRef(0);
const { data } = useQuery(getCallbackMythicTreeGroups, {
fetchPolicy: "no-cache",
variables: {callback_id: props.callback_id},
onCompleted: data => {
setGroups(data.callback_by_pk.mythictree_groups);
callbackDisplayID.current = data.callback_by_pk.display_id;
let otherGroupOptions = new Set([]);
for(let i = 0; i < data.callback.length; i++){
if(data.callback[i].mythictree_groups.length > 0){
@@ -109,7 +112,7 @@ export function ModifyCallbackMythicTreeGroupsDialog(props){
return (
<React.Fragment>
<DialogTitle id="form-dialog-title" style={{display: "flex", justifyContent: "space-between"}}>
Updating Callback Groups for Callback {props.callback_id}
Updating Callback Groups for Callback {callbackDisplayID.current}
<MythicStyledTooltip title="View all groups" >
<IconButton size="small" onClick={()=>{setOpenViewAllCallbacksDialog(true);}} style={{color: theme.palette.info.main}} variant="contained"><LayersIcon/></IconButton>
</MythicStyledTooltip>
@@ -1,6 +1,5 @@
import React, {useEffect} from 'react';
import {gql, useLazyQuery, useReactiveVar, useSubscription} from '@apollo/client';
import {meState} from '../../../cache';
import {gql, useLazyQuery, useSubscription} from '@apollo/client';
import {snackActions} from '../../utilities/Snackbar';
import {ResponseDisplayScreenshot} from './ResponseDisplayScreenshot';
import {ResponseDisplayPlaintext} from './ResponseDisplayPlaintext';
@@ -15,16 +14,16 @@ import {MythicStyledTooltip} from '../../MythicComponents/MythicStyledTooltip';
import Pagination from '@mui/material/Pagination';
import {ResponseDisplayInteractive} from "./ResponseDisplayInteractive";
import {ResponseDisplayMedia} from "./ResponseDisplayMedia";
import {ResponseDisplayMaterialReactTable} from "./ResponseDisplayMaterialReactTable";
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!){
response_stream(
batch_size: 50,
cursor: {initial_value: {id: 0}},
cursor: {initial_value: {timestamp: "1970-01-01"}},
where: {task_id: {_eq: $task_id} }
){
id
@@ -62,8 +61,8 @@ query subResponsesQuery($task_id: Int!, $search: String!) {
}
}`;
const taskScript = gql`
query getBrowserScriptsQuery($command_id: Int!, $operator_id: Int!){
browserscript(where: {active: {_eq: true}, command_id: {_eq: $command_id}, for_new_ui: {_eq: true}, operator_id: {_eq: $operator_id}}) {
query getBrowserScriptsQuery($command_id: Int!){
browserscript(where: {active: {_eq: true}, command_id: {_eq: $command_id}, for_new_ui: {_eq: true}}) {
script
id
}
@@ -80,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));
@@ -98,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: 10, 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);
@@ -125,7 +130,6 @@ const NonInteractiveResponseDisplay = (props) => {
}, b64DecodeUnicode(""));
setOutput(responses);
// update maxID
if(!props.selectAllOutput){
setTotalCount(data.response_aggregate.aggregate.count);
}
@@ -148,6 +152,7 @@ const NonInteractiveResponseDisplay = (props) => {
setTotalCount(1);
setOpenBackdrop(false);
togglingAllOutputToPaginated.current = true;
},
onError: (data) => {
@@ -163,52 +168,60 @@ 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
const currentIDS = rawResponses.map( r => r.id);
let newTotal = totalCount;
const newerResponses = data.data.response_stream.reduce( (prev, cur) => {
if(currentIDS.includes(cur.id)){return prev}
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)};
return prev;
}
return [...prev, {...cur, response: b64DecodeUnicode(cur.response)}]
}, 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) => {
@@ -226,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",
@@ -279,19 +292,22 @@ 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}
output={output} command_id={props.command_id} displayType={"accordion"}
task={props.task} search={search.current} expand={props.expand}/>
</div>
@@ -302,7 +318,7 @@ const NonInteractiveResponseDisplay = (props) => {
</div>
}
</React.Fragment>
</>
)
}
export const ResponseDisplayConsole = (props) => {
@@ -331,9 +347,12 @@ export const NonInteractiveResponseDisplayConsole = (props) => {
setRawResponses(responseArray);
} else {
// we still have some room to view more, but only room for initialResponseStreamLimit - totalFetched.current
const currentIDS = rawResponses.map( r => r.id);
const newerResponses = data.data.response_stream.reduce( (prev, cur) => {
if(currentIDS.includes(cur.id)){return prev}
let prevIndex = prev.findIndex( (v,i,a) => v.id === cur.id);
if(prevIndex >= 0){
prev[prevIndex] = {...cur, response: b64DecodeUnicode(cur.response)};
return prev;
}
return [...prev, {...cur, response: b64DecodeUnicode(cur.response)}]
}, rawResponses);
// sort them to make sure we're still in order
@@ -350,20 +369,16 @@ 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}>
<div style={{overflowY: "auto", width: "100%", height: props.expand ? "100%" : undefined}}
ref={props.responseRef}>
<ResponseDisplayComponent rawResponses={rawResponses} viewBrowserScript={props.viewBrowserScript}
output={output} command_id={props.command_id}
output={output} command_id={props.command_id} displayType={"console"}
task={props.task} search={""} expand={props.expand}/>
</div>
<div id={'scrolltotaskbottom' + props.task.id}></div>
</React.Fragment>
)
}
@@ -393,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 (
@@ -429,27 +444,11 @@ export const SearchBar = ({onSubmitSearch}) => {
);
}
const ResponseDisplayComponent = ({rawResponses, viewBrowserScript, output, command_id, task, search, expand}) => {
const ResponseDisplayComponent = ({rawResponses, viewBrowserScript, output, command_id, task, search, expand, displayType}) => {
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();
const me = useReactiveVar(meState);
useEffect( () => {
if(script.current !== undefined){
try{
const rawResponseData = rawResponses.map(c => c.response);
let res = script.current(task, rawResponseData);
setBrowserScriptData(filterOutput(res));
}catch(error){
setViewBrowserScript(false);
setBrowserScriptData({});
console.log(error);
}
}
}, [rawResponses, task]);
const script = React.useRef(undefined);
const filterOutput = (scriptData) => {
if(search === ""){
return scriptData;
@@ -461,67 +460,71 @@ const ResponseDisplayComponent = ({rawResponses, viewBrowserScript, output, comm
copied["plaintext"] = "";
}
}
if(useNewBrowserScriptTable){return copied}
if(scriptData["table"] !== undefined){
if(scriptData["table"].length > 0){
const tableUpdates = scriptData.table.map( t => {
const filteredRows = t.rows.filter( r => {
copied["table"] = scriptData.table.map(t => {
const filteredRows = t.rows.filter(r => {
let foundMatch = false;
for (const entry of Object.values(r)) {
if(entry["plaintext"] !== undefined){
if(String(entry["plaintext"]).toLowerCase().includes(search)){foundMatch = true;}
if (entry["plaintext"] !== undefined) {
if (String(entry["plaintext"]).toLowerCase().includes(search)) {
foundMatch = true;
}
}
if(entry["button"] !== undefined && entry["button"]["value"] !== undefined){
if(JSON.stringify(entry["button"]["value"]).includes(search)){foundMatch = true;}
if (entry["button"] !== undefined && entry["button"]["value"] !== undefined) {
if (JSON.stringify(entry["button"]["value"]).includes(search)) {
foundMatch = true;
}
}
}
return foundMatch;
});
return {...t, rows: filteredRows};
});
copied["table"] = tableUpdates;
}
}
return copied;
}
useEffect( () => {
if(loadingBrowserScript){
return;
}
if(script.current === undefined){
setViewBrowserScript(false);
}else{
setViewBrowserScript(viewBrowserScript);
if(viewBrowserScript){
try{
const rawResponseData = rawResponses.map(c => c.response);
let scriptTaskData = JSON.parse(JSON.stringify(task));
let res = script.current(scriptTaskData, rawResponseData);
setBrowserScriptData(filterOutput(res));
}catch(error){
setViewBrowserScript(false);
}
}
return;
}
}, [viewBrowserScript]);
if(viewBrowserScript){
try{
const rawResponseData = rawResponses.map(c => c.response);
let res = script.current(task, rawResponseData);
if(Object.keys(res).length === 0){
// if we run the browser script and there's no data, just display plaintext
setViewBrowserScript(false);
return;
}
setViewBrowserScript(viewBrowserScript);
setBrowserScriptData(filterOutput(res));
}catch(error){
if(rawResponses.length > 0){
setViewBrowserScript(false);
setBrowserScriptData({});
console.log(error);
}
}
} else {
setViewBrowserScript(false);
}
}, [rawResponses, task, loadingBrowserScript, viewBrowserScript]);
const [fetchScripts] = useLazyQuery(taskScript, {
fetchPolicy: "no-cache",
onCompleted: (data) => {
if(data.browserscript.length > 0){
try{
//let unb64script = b64DecodeUnicode(data.browserscript[0]["script"]);
//script.current = Function('"use strict";return(' + unb64script + ')')();
script.current = Function(`"use strict";return(${data.browserscript[0]["script"]})`)();
setViewBrowserScript(true);
//console.log(rawResponses);
const rawResponseData = rawResponses.map(c => c.response);
let scriptTaskData = JSON.parse(JSON.stringify(task));
let res = script.current(scriptTaskData, rawResponseData);
setBrowserScriptData(filterOutput(res));
}catch(error){
//snackActions.error(error.toString());
script.current = undefined;
console.log(error);
setViewBrowserScript(false);
setBrowserScriptData({});
}
}else{
setViewBrowserScript(false);
@@ -536,46 +539,71 @@ const ResponseDisplayComponent = ({rawResponses, viewBrowserScript, output, comm
useEffect( () => {
if(command_id !== undefined){
setLoadingBrowserScript(true);
fetchScripts({variables: {command_id: command_id, operator_id: me.user.user_id}});
fetchScripts({variables: {command_id: command_id}});
}
}, [command_id, task.id]);
if(loadingBrowserScript){
return null
}
return (
localViewBrowserScript && browserScriptData ? (
<React.Fragment>
{browserScriptData?.screenshot?.map( (scr, index) => (
<ResponseDisplayScreenshot key={"screenshot" + index + 'fortask' + task.id} task={task} {...scr} />
)) || null
}
{browserScriptData?.plaintext !== undefined &&
<ResponseDisplayPlaintext plaintext={browserScriptData["plaintext"]} task={task} expand={expand} />
}
{browserScriptData?.table?.map( (table, index) => {
if(useNewBrowserScriptTable){
return <ResponseDisplayMaterialReactTable callback_id={task.callback_id} task={task} expand={expand} table={table} key={"tablefortask" + task.id + "table" + index} />
}
return <ResponseDisplayTable callback_id={task.callback_id} task={task} expand={expand} table={table} key={"tablefortask" + task.id + "table" + index} />
}) || null
}
{browserScriptData?.download?.map( (dl, index) => (
<ResponseDisplayDownload download={dl} task={task} key={"download" + index + "fortask" + task.id} />
)) || null
}
{browserScriptData?.search?.map( (s, index) => (
<ResponseDisplaySearch search={s} task={task} key={"searchlink" + index + "fortask" + task.id} />
)) || null
}
{browserScriptData?.media?.map( (s, index) => (
<ResponseDisplayMedia key={"searchmedia" + index + "fortask" + task.id} task={task} media={s} expand={expand} />
)) || null}
{browserScriptData?.graph !== undefined &&
<ResponseDisplayGraph graph={browserScriptData.graph} task={task} expand={expand} />
}
</React.Fragment>
localViewBrowserScript ? (
<ResponseDisplayBrowserScriptComponent expand={expand} displayType={displayType}
task={task} browserScriptData={browserScriptData}
output={output}/>
) : (
<ResponseDisplayPlaintext plaintext={output} task={task} expand={expand}/>
<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}/>
)
}
</>
)
}
@@ -6,7 +6,7 @@ import { gql, useMutation, useSubscription } from '@apollo/client';
import {b64DecodeUnicode} from "./ResponseDisplay";
import {SearchBar} from './ResponseDisplay';
import Pagination from '@mui/material/Pagination';
import {Typography, CircularProgress, Select, IconButton} from '@mui/material';
import {Typography, CircularProgress, Select, IconButton, Backdrop} from '@mui/material';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import Input from '@mui/material/Input';
import MenuItem from '@mui/material/MenuItem';
@@ -18,6 +18,10 @@ import KeyboardReturnIcon from '@mui/icons-material/KeyboardReturn';
import InputLabel from "@mui/material/InputLabel";
import FormControl from "@mui/material/FormControl";
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}
@@ -37,10 +41,13 @@ subscription subResponsesQuery($task_id: Int!) {
}
}`;
const getTaskingStatus = (task) => {
if(task.status === "completed"){
if(task.status === "completed" || task.status === "success"){
return <CheckCircleOutlineIcon color={"success"} fontSize={"1rem"} style={{marginRight: "2px"}} />
} else if(task.status === "submitted"){
return <CircularProgress size={"1rem"} />
} else {
console.log(task.status);
return null
}
}
const getClassnames = (entry) => {
@@ -73,57 +80,89 @@ const getClassnames = (entry) => {
//console.log(entry);
return classnames.join(" ");
}
const GetOutputFormat = ({data, useASNIColor, messagesEndRef, showTaskStatus, wrapText}) => {
const handleTerminalCodes = (response) => {
let output = response.replaceAll("[?2004h", "");
output = output.replaceAll("[?2004l", "");
let indexOfTitleSeq = output.indexOf("]0");
if(indexOfTitleSeq >= 0){
let endIndexOfTitleSeq = output.indexOf("]");
if(endIndexOfTitleSeq >= 0 && endIndexOfTitleSeq > indexOfTitleSeq){
output = output.substring(0, indexOfTitleSeq + 2) + output.substring(endIndexOfTitleSeq);
}
}
let indexOfClearLeft = output.indexOf("[J");
if(indexOfClearLeft >= 0){
let indexOfLastNewLine = 0;
for(let i = indexOfClearLeft; i >= 0; i--){
if(output[i] === "\n"){
indexOfLastNewLine = i;
break;
}
}
output = output.substring(0, indexOfLastNewLine+1) + output.substring(indexOfClearLeft + 2);
}
return output;
}
export const GetOutputFormatAll = ({data, myTask, taskID, useASNIColor, messagesEndRef, showTaskStatus, wrapText, search}) => {
const [dataElement, setDataElement] = React.useState(null);
React.useEffect( () => {
if(data.response) {
// we're looking at response output
if(data.is_error){
setDataElement(<pre style={{display: "inline",backgroundColor: "#311717", margin: "0 0 0 0",
wordBreak: wrapText ? "break-all" : "",
whiteSpace: wrapText ? "pre-wrap" : ""}} key={data.timestamp + data.id}>
{data.response}
</pre>)
} else {
if(useASNIColor){
let ansiJSON = Anser.ansiToJson(data.response, { use_classes: true });
//console.log(ansiJSON)
setDataElement(
ansiJSON.map( (a, i) => (
<pre style={{display: "inline", margin: "0 0 0 0",
wordBreak: wrapText ? "break-all" : "",
whiteSpace: wrapText ? "pre-wrap" : "",
}} className={getClassnames(a)} key={data.id + data.timestamp + i}>{a.content}</pre>
))
)
} else {
setDataElement(<pre style={{display: "inline", margin: "0 0 0 0",
const elements = data.map( d => {
if(d.response !== undefined) {
// we're looking at response output
if(d.is_error){
return (<pre id={"response" + d.timestamp + d.id} style={{display: "inline",backgroundColor: "#311717", color: "white", margin: "0 0 0 0",
wordBreak: wrapText ? "break-all" : "",
whiteSpace: wrapText ? "pre-wrap" : "",
}} key={data.timestamp + data.id}>{data.response}</pre>)
}
whiteSpace: wrapText ? "pre-wrap" : ""}} key={d.timestamp + d.id}>
{d.response}
</pre>)
} else {
if(useASNIColor){
let removedTerminalCodes = handleTerminalCodes(d.response);
let ansiJSON = Anser.ansiToJson(removedTerminalCodes, { use_classes: true });
//console.log(ansiJSON)
return (
ansiJSON.map( (a, i) => (
<pre id={"response" + d.timestamp + d.id} style={{display: "inline", margin: "0 0 0 0",
wordBreak: wrapText ? "break-all" : "",
whiteSpace: wrapText ? "pre-wrap" : "",
}} className={getClassnames(a)} key={d.id + d.timestamp + i}>{a.content}</pre>
))
)
} else {
return (<pre id={"response" + d.timestamp + d.id} style={{display: "inline", margin: "0 0 0 0",
wordBreak: wrapText ? "break-all" : "",
whiteSpace: wrapText ? "pre-wrap" : "",
}} key={d.timestamp + d.id}>{d.response}</pre>)
}
}
} else {
// we're looking at tasking
setDataElement(
<pre key={data.timestamp + data.id} style={{display: "inline",margin: "0 0 0 0",
wordBreak: wrapText ? "break-all" : "", whiteSpace: "pre-wrap"}}>
{showTaskStatus && getTaskingStatus(data)}
{data.original_params}
}
} else {
// we're looking at tasking
return(
<pre id={"task" + d.timestamp + d.id} key={d.timestamp + d.id} style={{display: "inline",margin: "0 0 0 0",
wordBreak: wrapText ? "break-all" : "", whiteSpace: "pre-wrap"}}>
{showTaskStatus && getTaskingStatus(d)}
{d.original_params}
</pre>
)
}
}, [data.timestamp, useASNIColor, showTaskStatus, wrapText]);
/*
React.useEffect( () => {
if(messagesEndRef.current){
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
)
}
})
setDataElement(elements);
}, [data, useASNIColor, showTaskStatus, wrapText]);
React.useLayoutEffect( () => {
if(myTask){
let el = document.getElementById(`ptytask${taskID}`);
if(el && el.scrollHeight - el.scrollTop - el.clientHeight < 500){
if(!search){
messagesEndRef?.current?.scrollIntoView({ behavior: "auto", block: "nearest" });
//console.log("scrolling");
}
} else {
// console.log("not scrolled down enough")
}
}
}, [dataElement]);
*/
return (
dataElement
)
@@ -134,6 +173,7 @@ const InteractiveMessageTypes = [
{"name": "None", "value": -1, "text": "None"},
{"name": "Tab", "value": 13, "text": "^I"},
{"name": "Backspace", "value": 12, "text": "^H"},
{"name": "Exit", "value": 3, "text": "exit"},
{"name": "Escape", "value": 4, "text": "^["},
{"name": "Ctrl+A", "value": 5, "text": "^A"},
{"name": "Ctrl+B", "value": 6, "text": "^B"},
@@ -161,6 +201,10 @@ const EnterOptions = [
{"value": "\r\n", "name": "CRLF"}
];
export const ResponseDisplayInteractive = (props) =>{
const me = useReactiveVar(meState);
const theme = useTheme();
const [backdropOpen, setBackdropOpen] = React.useState(false);
const [scrollToBottom, setScrollToBottom] = React.useState(true);
const pageSize = React.useRef(100);
const highestFetched = React.useRef(0);
const [taskData, setTaskData] = React.useState([]);
@@ -175,9 +219,9 @@ export const ResponseDisplayInteractive = (props) =>{
const [useASNIColor, setUseANSIColor] = React.useState(true);
const [showTaskStatus, setShowTaskStatus] = React.useState(true);
const [wrapText, setWrapText] = React.useState(true);
useSubscription(getInteractiveTaskingQuery, {
const [autoScroll, setAutoScroll] = React.useState(true);
const {loading: loadingTasks} = useSubscription(getInteractiveTaskingQuery, {
variables: {parent_task_id: props.task.id},
shouldResubscribe: true,
onError: data => {
console.error(data)
},
@@ -198,15 +242,19 @@ export const ResponseDisplayInteractive = (props) =>{
}, [...taskData]);
setTaskData(newTaskData);
}
if(backdropOpen){
setBackdropOpen(false);
}
}
})
})
const subscriptionDataCallback = React.useCallback( ({data}) => {
// we still have some room to view more, but only room for fetchLimit - totalFetched.current
if(props.task.id !== taskIDRef.current){
const newResponses = data.data.response_stream;
const newerResponses = newResponses.map( (r) => { return {...r, response: b64DecodeUnicode(r.response)}});
newerResponses.sort( (a,b) => a.id > b.id ? 1 : -1);
let rawResponseArray = [...rawResponses];
let rawResponseArray = [];
let highestFetchedId = 0;
for(let i = 0; i < newerResponses.length; i++){
rawResponseArray.push(newerResponses[i]);
@@ -216,23 +264,27 @@ export const ResponseDisplayInteractive = (props) =>{
highestFetched.current = highestFetchedId;
taskIDRef.current = props.task.id;
} else {
const newResponses = data.data.response_stream.filter( r => r.id > highestFetched.current);
const newerResponses = newResponses.map( (r) => { return {...r, response: b64DecodeUnicode(r.response)}});
newerResponses.sort( (a,b) => a.id > b.id ? 1 : -1);
const newResponses = data.data.response_stream.filter(r => r.id > highestFetched.current);
const newerResponses = newResponses.map((r) => {
return {...r, response: b64DecodeUnicode(r.response)}
});
newerResponses.sort((a, b) => a.id > b.id ? 1 : -1);
let rawResponseArray = [...rawResponses];
let highestFetchedId = highestFetched.current;
for(let i = 0; i < newerResponses.length; i++){
for (let i = 0; i < newerResponses.length; i++) {
rawResponseArray.push(newerResponses[i]);
highestFetchedId = newerResponses[i]["id"];
}
setRawResponses(rawResponseArray);
highestFetched.current = highestFetchedId;
}
}, [highestFetched.current, rawResponses, props.task.id]);
if(backdropOpen){
setBackdropOpen(false);
}
}, [highestFetched.current, rawResponses, props.task.id, backdropOpen, taskIDRef.current, autoScroll]);
useSubscription(subResponsesQuery, {
variables: {task_id: props.task.id},
fetchPolicy: "network_only",
fetchPolicy: "no-cache",
onData: subscriptionDataCallback
});
const onSubmitPageChange = (currentPage) => {
@@ -240,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];
@@ -265,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;
});
@@ -286,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);
}, []);
@@ -312,50 +368,93 @@ export const ResponseDisplayInteractive = (props) =>{
const toggleWrapText = () => {
setWrapText(!wrapText);
}
const scrollContent = (node, isAppearing) => {
// only auto-scroll if you issued the task
document.getElementById(`scrolltotaskbottom${props.task.id}`)?.scrollIntoView({
//behavior: "smooth",
block: "end",
inline: "nearest"
})
const toggleAutoScroll = () => {
setAutoScroll(!autoScroll);
}
React.useLayoutEffect( () => {
scrollContent()
}, []);
useEffect(() => {
if(autoScroll){
messagesEndRef?.current?.scrollIntoView({ behavior: "auto", block: "nearest" });
}
}, [props.responseRef?.current?.scrollHeight, autoScroll]);
React.useEffect( () => {
if(loadingTasks){
setTaskData([]);
setBackdropOpen(true);
}else{
setBackdropOpen(false);
}
}, [loadingTasks]);
setTimeout(() => {
// close the backdrop after 2 seconds in case there's no data to fetch
setBackdropOpen(false);
}, 2000);
return (
<div style={{ display: "flex", overflowY: "auto",
position: "relative", height: props.expand ? "100%": undefined, maxHeight: props.expand ? "100%": "500px",
flexDirection: "column"}}>
{props.searchOutput &&
<SearchBar onSubmitSearch={onSubmitSearch} />
}
<div style={{overflowY: "auto", width: "100%", flexGrow: 1}} ref={props.responseRef}>
{alloutput.map( (e, index) => (
<GetOutputFormat key={"getoutput" + index} data={e} useASNIColor={useASNIColor} messagesEndRef={messagesEndRef}
showTaskStatus={showTaskStatus} wrapText={wrapText} />
))}
<div ref={messagesEndRef}/>
</div>
<div style={{
display: "flex", overflowY: "auto",
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={{
borderRadius: "4px",
border: "1px solid black",
padding: "5px",
backgroundColor: "rgba(37,37,37,0.92)", color: "white",
alignItems: "center",
display: "flex", flexDirection: "column"}}>
<CircularProgress color="inherit" />
<Typography variant={"h5"}>
Fetching Interactive Task Data....
</Typography>
</div>
</Backdrop>
{props.searchOutput &&
<SearchBar onSubmitSearch={onSubmitSearch}/>
}
<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}/>
<div ref={messagesEndRef}/>
</div>
{!props.task?.is_interactive_task &&
<div style={{width: "100%", display: "inline-flex", alignItems: "flex-end"}}>
<InteractiveTaskingBar task={props.task} taskData={taskData}
useASNIColor={useASNIColor} toggleANSIColor={toggleANSIColor}
showTaskStatus={showTaskStatus} toggleShowTaskStatus={toggleShowTaskStatus}
wrapText={wrapText} toggleWrapText={toggleWrapText}
autoScroll={autoScroll} toggleAutoScroll={toggleAutoScroll}
/>
</div>
}
<div style={{width: "100%", display: "inline-flex", alignItems: "flex-end"}} >
<InteractiveTaskingBar task={props.task} taskData={taskData}
useASNIColor={useASNIColor} toggleANSIColor={toggleANSIColor}
showTaskStatus={showTaskStatus} toggleShowTaskStatus={toggleShowTaskStatus}
wrapText={wrapText} toggleWrapText={toggleWrapText}
/>
</div>
<InteractivePaginationBar totalCount={totalCount} currentPage={page.current}
onSubmitPageChange={onSubmitPageChange} expand={props.expand}
pageSize={pageSize.current} />
</div>
<InteractivePaginationBar totalCount={totalCount} currentPage={page.current}
onSubmitPageChange={onSubmitPageChange} expand={props.expand}
pageSize={pageSize.current} selectAllOutput={props.selectAllOutput}/>
</div>
)
}
const InteractiveTaskingBar = ({task, taskData, useASNIColor, toggleANSIColor,
showTaskStatus, toggleShowTaskStatus, wrapText, toggleWrapText}) => {
const InteractiveTaskingBar = ({
task, taskData, useASNIColor, toggleANSIColor,
showTaskStatus, toggleShowTaskStatus, wrapText, toggleWrapText,
autoScroll, toggleAutoScroll
}) => {
const [inputText, setInputText] = React.useState("");
const theme = useTheme();
const [createTask] = useMutation(createTaskingMutation, {
update: (cache, {data}) => {
if(data.createTask.status === "error"){
@@ -369,65 +468,74 @@ const InteractiveTaskingBar = ({task, taskData, useASNIColor, toggleANSIColor,
});
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(selectedControlOption.value > 0){
let ctrlSequence = selectedControlOption.text;
let enterOption = selectedEnterOption.value;
if(event.metaKey || event.ctrlKey){
setInputText(inputText + EnterOptions[selectedEnterOption].value);
return;
}
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 = "";
}
@@ -440,16 +548,16 @@ const InteractiveTaskingBar = ({task, taskData, useASNIColor, toggleANSIColor,
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,
@@ -457,7 +565,8 @@ const InteractiveTaskingBar = ({task, taskData, useASNIColor, toggleANSIColor,
}})
}
setInputText("");
setSelectedControlOption(InteractiveMessageTypes[0]);
setSelectedControlOption(0);
setTaskOptionsIndex(-1);
}
const onChangeSelect = (event) => {
event.stopPropagation();
@@ -470,86 +579,102 @@ const InteractiveTaskingBar = ({task, taskData, useASNIColor, toggleANSIColor,
setSelectedEnterOption(event.target.value);
}
return (
<>
<FormControl style={{width: "10rem", marginTop: "2px"}} >
<InputLabel id="control-label" style={{}}>Controls</InputLabel>
<div style={{
display: "flex",
alignItems: "flex-end",
//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", 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", paddingBottom: "10px",}} >
<div style={{background: "transparent", display: "flex", justifyContent: "center", alignItems: "center"}} >
<Pagination count={pageCount} page={currentPage} variant="contained" color="primary"
boundaryCount={2} onChange={onChangePage} style={{margin: "10px"}}
showFirstButton showLastButton siblingCount={2}
@@ -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';
@@ -10,6 +9,7 @@ import {useTheme} from '@mui/material/styles';
import {snackActions} from "../../utilities/Snackbar";
import FormControl from '@mui/material/FormControl';
import WrapTextIcon from '@mui/icons-material/WrapText';
// https://github.com/ajaxorg/ace-builds/tree/master/src-min-noconflict
import 'ace-builds/src-noconflict/mode-json';
import 'ace-builds/src-noconflict/mode-csharp';
import 'ace-builds/src-noconflict/mode-golang';
@@ -21,26 +21,231 @@ import 'ace-builds/src-noconflict/mode-java';
import 'ace-builds/src-noconflict/mode-javascript';
import 'ace-builds/src-noconflict/theme-monokai';
import 'ace-builds/src-noconflict/theme-xcode';
import 'ace-builds/src-noconflict/mode-yaml';
import 'ace-builds/src-noconflict/mode-toml';
import 'ace-builds/src-noconflict/mode-swift';
import 'ace-builds/src-noconflict/mode-sql';
import 'ace-builds/src-noconflict/mode-rust';
import 'ace-builds/src-noconflict/mode-powershell';
import 'ace-builds/src-noconflict/mode-pgsql';
import 'ace-builds/src-noconflict/mode-perl';
import 'ace-builds/src-noconflict/mode-php';
import 'ace-builds/src-noconflict/mode-objectivec';
import 'ace-builds/src-noconflict/mode-nginx';
import 'ace-builds/src-noconflict/mode-makefile';
import 'ace-builds/src-noconflict/mode-kotlin';
import 'ace-builds/src-noconflict/mode-dockerfile';
import 'ace-builds/src-noconflict/mode-sh';
import 'ace-builds/src-noconflict/mode-ini';
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 {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}}){
id
host
filename_text
full_remote_path_text
size
agent_file_id
complete
total_chunks
chunks_received
task {
display_id
id
}
tags {
tagtype {
name
color
id
}
id
}
}
}
`
export const ResponseDisplayMedia = ({media, expand, task}) =>{
return <DisplayMedia agent_file_id={media?.agent_file_id || ""} task={task} filename={media?.filename || undefined} expand={expand} />
const displayType = mimeType(media?.filename);
const [value, setValue] = React.useState(displayType !== undefined ? 0 : 1);
const [fileMetaData, setFileMetaData] = React.useState({});
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);
}
return (
<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}
indicatorColor='secondary'
textColor='primary'
sx={{
'& .Mui-selected': {
color: "unset !important"
}
}}
variant='scrollable'
scrollButtons='auto'
style={{maxWidth: '100%', width: '100%'}}
aria-label='scrollable auto tabs example'>
<Tab className={value === 0 ? "selectedCallback": ""} label={"Preview"}></Tab>
<Tab className={value === 1 ? "selectedCallback": ""} label={"Text"}></Tab>
<Tab className={value === 2 ? "selectedCallback": ""} label={"Hex"}></Tab>
<Tab className={value === 3 ? "selectedCallback": ""} label={"Database"}></Tab>
<MythicStyledTooltip title={"Download the file"} tooltipStyle={{display: "inline-flex"}}>
<Button style={{}} size={"small"} href={"/direct/download/" + media.agent_file_id}
download color={"success"}>
<DownloadIcon />
</Button>
</MythicStyledTooltip>
</Tabs>
</AppBar>
<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}
fileMetaData={fileMetaData}
expand={expand} />
}
</div>
<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}
fileMetaData={fileMetaData}
expand={expand} preview />
}
</div>
<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}
fileMetaData={fileMetaData}
expand={expand} />
}
</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>
)
}
const textExtensionTypes = ["txt", "ps1", "php", "json", "yml", "yaml", "config", "cfg", "go",
export const textExtensionTypes = ["txt", "ps1", "php", "json", "yml", "yaml", "config", "cfg", "go",
"html", "xml", "js", "java", "conf", "cs", "rb", "toml", "sh", "md", "ini", "py", "kirbi", "bash_profile", "rc",
"local", "gitconfig", "gitignore", "zsh_history", "bash_history", "ps", "psql_history", "lesshst", "gcloudignore",
"pem", "boto", "zsh_profile", "pub", "python_history", "sqlite_history", "viminfo", "zprofile", "zshrc",
"history", "historynew"
"history", "historynew", "bashrc",
];
const knownTextFiles = ["config", "credentials", "known_hosts", "config_default", "id_rsa"];
const imgExtensionTypes = ["png", "jpg", "gif", "jpeg", "pdf"];
export const textExtensionTypesToSyntax = {
"json": "json",
"cs": "csharp",
"go": "golang",
"md": "markdown",
"MD": "markdown",
"rb": "ruby",
"py": "python",
"java": "java",
"js": "javascript",
"yml": "yaml",
"yaml": "yaml",
"toml": "toml",
"swift": "swift",
"psql": "sql",
"rs": "rust",
"ps1": "powershell",
"prl": "perl",
"php": "php",
"m": "objectivec",
"mm": "objectivec",
"dockerfile": "dockerfile",
"makefile": "makefile",
"sh": "sh",
"rc": "sh",
"bashrc": "sh",
"zshrc": "sh",
"zprofile": "sh",
"bash_profile": "sh",
"ini": "ini",
"conf": "apache_conf",
"bash_history": "sh",
"zsh_history": "sh",
"python_history": "python",
}
const knownTextFiles = ["config", "credentials", "known_hosts", "config_default", "id_rsa", "dockerfile", "makefile"];
const imgExtensionTypes = ["png", "jpg", "gif", "jpeg", "svg"];
const renderExtensionTypes = ["pdf"];
const mimeType = (path) => {
if(!path){return undefined}
let extension = path.split(".");
if(extension.length > 0){
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())){
@@ -52,8 +257,9 @@ const mimeType = (path) => {
}
return undefined;
}
export const DisplayMedia = ({agent_file_id, filename, expand, task}) => {
const showMediaSetting = useMythicSetting({setting_name: "showMedia", default_value: "true"});
export const DisplayMedia = ({agent_file_id, filename, expand, task, fileMetaData}) => {
const showMediaSetting = GetMythicSetting({setting_name: "showMedia", default_value: true});
const [openScreenshot, setOpenScreenshot] = React.useState(false);
const [showMedia, setShowMedia] = React.useState(showMediaSetting);
const [fileData, setFileData] = React.useState({
display: false,
@@ -93,6 +299,11 @@ export const DisplayMedia = ({agent_file_id, filename, expand, task}) => {
React.useLayoutEffect( () => {
scrollContent()
}, []);
const clickedScreenshot = (e) => {
e.preventDefault();
e.stopPropagation();
setOpenScreenshot(true);
}
if(!showMedia){
return (
<>
@@ -129,57 +340,185 @@ export const DisplayMedia = ({agent_file_id, filename, expand, task}) => {
</object>
)
}
if(fileData.display_type === "text"){
if(fileData.display_type === "image"){
return (
<div style={{height: "100%", minHeight: "200px", width: "100%"}} >
<DisplayText agent_file_id={agent_file_id} expand={expand} />
</div>
<>
<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 DisplayText = ({agent_file_id, expand}) => {
const MaxRenderSize = 2000000; // 2MB
const DisplayFileMetaData = ({fileMetaData}) => {
return (
<Table style={{marginLeft: "0px", width: "100%", tableLayout: "fixed"}}>
<TableHead>
<TableRow>
<MythicStyledTableCell style={{width: "7rem"}}>Size</MythicStyledTableCell>
<MythicStyledTableCell>Host</MythicStyledTableCell>
<MythicStyledTableCell>File</MythicStyledTableCell>
<MythicStyledTableCell>Path</MythicStyledTableCell>
<MythicStyledTableCell style={{width: "5rem"}}>Task</MythicStyledTableCell>
<MythicStyledTableCell style={{}}>Tags</MythicStyledTableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<MythicStyledTableCell>
<TableRowSizeCell cellData={fileMetaData.size}/>
{fileMetaData.complete ? null : (
<>
<br/>
<Typography style={{display: "inline-block", color: "red"}}>
{fileMetaData.chunks_received}
</Typography>
/
<Typography style={{display: "inline-block"}}>
{fileMetaData.total_chunks}
</Typography>
<Typography style={{display: "inline-block", whiteSpace: "pre"}}>
{" Chunks"}
</Typography>
</>
)}
</MythicStyledTableCell>
<MythicStyledTableCell style={{wordBreak: "break-all"}}>{fileMetaData.host}</MythicStyledTableCell>
<MythicStyledTableCell style={{wordBreak: "break-all"}}>{fileMetaData.filename}</MythicStyledTableCell>
<MythicStyledTableCell style={{wordBreak: "break-all"}}>{fileMetaData.full_remote_path}</MythicStyledTableCell>
<MythicStyledTableCell><Link style={{wordBreak: "break-all"}}
color="textPrimary" underline="always"
target="_blank" href={"/new/task/" + fileMetaData.task_display_id}>
{fileMetaData.task_display_id}
</Link></MythicStyledTableCell>
<MythicStyledTableCell>
<ViewEditTags target_object={"filemeta_id"} target_object_id={fileMetaData.id} />
<TagsDisplay tags={fileMetaData.tags}/>
</MythicStyledTableCell>
</TableRow>
</TableBody>
</Table>
)
}
const DisplayText = ({agent_file_id, expand, filename, preview, fileMetaData}) => {
const theme = useTheme();
const [mode, setMode] = React.useState("html");
const [content, setContent] = React.useState("");
const [wrapText, setWrapText] = React.useState(true);
React.useEffect( () => {
fetch('/direct/view/' + agent_file_id).then((response) => {
if(response.status !== 200){
snackActions.warning("Failed to fetch contents from Mythic");
return;
}
response.text().then(data => {
if(data.length > MaxRenderSize){
snackActions.warning("File too large (> 2MB), truncating the render");
const [previewFileString] = useMutation(previewFileQuery, {
onCompleted: (data) => {
if(data.previewFile.status === "success"){
setContent(b64DecodeUnicode(data.previewFile.contents));
if(data.previewFile.size > 512000){
setLimitedPreviewWarning(true);
}
}else{
console.log(data.previewFile.error);
if(data.previewFile.error !== "EOF"){
snackActions.error(data.previewFile.error);
}
setContent(data.substring(0, MaxRenderSize));
}).catch(error => {
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());
}
console.log("There was an error!", error);
});
}, []);
},
onError: (data) => {
console.log(data);
snackActions.error(data);
}
});
const [limitedPreviewWarning, setLimitedPreviewWarning] = React.useState(false);
const currentContentRef = React.useRef();
React.useEffect( () => {
if(preview){
// get first 512KB
previewFileString({variables: {file_id: agent_file_id}})
}else{
// get entire file
fetch('/direct/view/' + agent_file_id).then((response) => {
if(response.status !== 200){
snackActions.warning("Failed to fetch contents from Mythic");
return;
}
response.text().then(data => {
if(data.length > MaxRenderSize){
snackActions.warning("File too large (> 2MB), truncating the render");
setLimitedPreviewWarning(true);
setContent(data.substring(0, MaxRenderSize));
return;
}
try{
let cont = JSON.stringify(JSON.parse(data), null, 2);
setContent(cont);
}catch(error){
setContent(data.substring(0, MaxRenderSize));
}
}).catch(error => {
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());
}
console.log("There was an error!", error);
});
}
if(filename){
let extension = filename.split(".");
if(extension.length > 1){
extension = extension[extension.length - 1];
if(textExtensionTypesToSyntax[extension]){
setMode(textExtensionTypesToSyntax[extension]);
}
} else {
if(textExtensionTypesToSyntax[filename.toLowerCase()]){
setMode(textExtensionTypesToSyntax[filename.toLowerCase()]);
}
}
}
}, [agent_file_id, filename, fileMetaData]);
const onChangeMode = (event) => {
setMode(event.target.value);
}
const toggleWrapText = () => {
setWrapText(!wrapText);
}
const formatJSON = () => {
try{
let tmp = JSON.stringify(JSON.parse(currentContentRef.current?.editor?.getValue()), null, 2);
setContent(tmp);
setMode("json");
}catch(error){
snackActions.warning("Failed to reformat as JSON")
}
}
return (
<div style={{display: "flex", height: "100%", flexDirection: "column"}}>
<div style={{display: "inline-flex", flexDirection: "row"}}>
<FormControl sx={{ display: "inline-block" }} size="small" color={"secondary"}>
<div style={{display: "inline-flex", flexDirection: "row", alignItems: "center"}}>
<FormControl sx={{ display: "inline-block", marginLeft: "10px" }} size="small" color={"secondary"}>
<TextField
select
label={"Syntax"}
@@ -204,9 +543,23 @@ const DisplayText = ({agent_file_id, expand}) => {
/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Auto format JSON"} >
<IconButton onClick={formatJSON} style={{}}>
<CodeIcon color={"info"} style={{cursor: "pointer"}} />
</IconButton>
</MythicStyledTooltip>
{limitedPreviewWarning &&
<>
<MythicStyledTooltip title={preview ? "Only first 512KB of file is shown" :
"Only first 2MB of file is shown"}>
<WarningOutlinedIcon style={{marginLeft: "5px"}} color={"warning"} />
</MythicStyledTooltip>
</>
}
</div>
<div style={{display: "flex", flexGrow: 1, height: "100%"}}>
<AceEditor
ref={currentContentRef}
mode={mode}
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
fontSize={14}
@@ -231,4 +584,326 @@ const DisplayText = ({agent_file_id, expand}) => {
</div>
)
}
const DisplayHex = ({agent_file_id, expand, fileMetaData}) => {
const theme = useTheme();
const [content, setContent] = React.useState("");
const [previewFileString] = useMutation(previewFileQuery, {
onCompleted: (previewData) => {
if(previewData.previewFile.status === "success"){
let data = atob(previewData.previewFile.contents);
let newData = "";
let stringData = "";
for(let i = 0; i < data.length; i++){
let char = data[i].charCodeAt(0).toString(16);
if(char.length === 1){
newData += "0" + char;
}else{
newData += char;
}
if(data[i] === "\n"){
stringData += "\\n";
}else if(data[i] === "\r"){
stringData += "\\r";
}else{
stringData += data[i];
}
if((i+1) % 4 === 0){
newData += " ";
}
if((i+1) % 32 === 0){
newData += ": " + stringData + "\n";
stringData = "";
}
}
setContent(newData);
}else{
snackActions.error(previewData.previewFile.error)
}
},
onError: (data) => {
console.log(data);
snackActions.error(data);
}
});
React.useEffect( () => {
previewFileString({variables: {file_id: agent_file_id}})
}, [agent_file_id])
return (
<div style={{display: "flex", height: "100%", flexDirection: "column"}}>
<div style={{display: "flex", flexGrow: 1, height: "100%"}}>
<AceEditor
mode="json"
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
fontSize={14}
showGutter={true}
//onLoad={onLoad}
highlightActiveLine={false}
showPrintMargin={false}
value={content}
height={expand ? "100%": undefined}
maxLines={expand ? undefined : 20}
width={"100%"}
//autoScrollEditorIntoView={true}
minLines={2}
//maxLines={props.expand ? 50 : 20}
setOptions={{
showLineNumbers: true,
tabSize: 4,
useWorker: false
}}/>
</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,45 +1,53 @@
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';
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) =>{
const theme = useTheme();
const me = useReactiveVar(meState);
const currentContentRef = React.useRef();
const [plaintextView, setPlaintextView] = React.useState("");
const [mode, setMode] = React.useState("html");
const [wrapText, setWrapText] = React.useState(true);
const initialMode = props?.initial_mode || "html";
const [mode, setMode] = React.useState(initialMode);
const [wrapText, setWrapText] = React.useState(props?.wrap_text === undefined ? true : props.wrap_text);
const [showOptions, setShowOptions] = React.useState(false);
const [renderColors, setRenderColors] = React.useState(props?.render_colors === undefined ? false : props.render_colors);
const onChangeText = (data) => {
if(props.onChangeContent){
props?.onChangeContent(data);
}
setPlaintextView(data);
}
useEffect( () => {
if(props.plaintext.length > MaxRenderSize){
snackActions.warning("Response too large (> 2MB), truncating the render. Download task output to view entire response.");
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));
}
@@ -51,31 +59,63 @@ export const ResponseDisplayPlaintext = (props) =>{
const toggleWrapText = () => {
setWrapText(!wrapText);
}
const formatJSON = () => {
try{
let tmp = JSON.stringify(JSON.parse(currentContentRef.current?.editor?.getValue()), null, 2);
setPlaintextView(tmp);
setMode("json");
}catch(error){
snackActions.warning("Failed to reformat as JSON")
}
}
const onChangeShowOptions = (e) => {
setShowOptions(!showOptions);
}
const scrollContent = (node, isAppearing) => {
// only auto-scroll if you issued the task
document.getElementById(`scrolltotaskbottom${props.task.id}`)?.scrollIntoView({
//behavior: "smooth",
block: "end",
inline: "nearest"
})
if(props?.task?.operator?.username === (me?.user?.username || "")){
let el = document.getElementById(`taskingPanel${props.task.callback_id}`);
if(props.expand || props.displayType === "console"){
el = document.getElementById(`taskingPanelConsole${props.task.callback_id}`);
}
if(el && el.scrollHeight - el.scrollTop - el.clientHeight < 600){
document.getElementById(`scrolltotaskbottom${props.task.id}`)?.scrollIntoView({
//behavior: "smooth",
block: "end",
inline: "nearest"
});
}
}
}
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"}}>
<FormControl sx={{ display: "inline-block" }} 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}
>
@@ -93,39 +133,81 @@ export const ResponseDisplayPlaintext = (props) =>{
/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Auto format JSON"} >
<IconButton onClick={formatJSON} style={{}}>
<CodeIcon color={"info"} style={{cursor: "pointer"}} />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={renderColors ? "Render Plaintext" : "Render Colors"} >
<IconButton onClick={() => {setRenderColors(!renderColors)}}>
<PaletteIcon color={renderColors ? "success" : "secondary"} style={{cursor: "pointer"}} />
</IconButton>
</MythicStyledTooltip>
</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={{height: "1px", width: "100%", display: "flex", zIndex: 1, justifyContent: "space-around", backgroundColor: theme.palette.secondary.main}}>
{showOptions &&
<UnfoldLessIcon onClick={onChangeShowOptions} style={{cursor: "pointer", position: "relative", top: "-8px"}} />
}
{!showOptions &&
<UnfoldMoreIcon onClick={onChangeShowOptions} style={{cursor: "pointer", position: "relative", top: "-7px"}} />
}
</div>
<div style={{display: "flex", flexGrow: 1, height: "100%"}}>
<AceEditor
mode={mode}
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
fontSize={14}
showGutter={true}
//onLoad={onLoad}
highlightActiveLine={false}
showPrintMargin={false}
value={plaintextView}
height={props.expand ? "100%": undefined}
maxLines={props.expand ? undefined : 20}
width={"100%"}
//autoScrollEditorIntoView={true}
wrapEnabled={wrapText}
minLines={1}
//maxLines={props.expand ? 50 : 20}
setOptions={{
showLineNumbers: true,
tabSize: 4,
useWorker: false
}}/>
<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"}]}
myTask={false}
taskID={props?.task?.id || 0}
useASNIColor={true}
messagesEndRef={null}
showTaskStatus={false}
search={undefined}
wrapText={wrapText}/>
}
{!renderColors &&
<AceEditor
className={"roundedBottomCorners"}
ref={currentContentRef}
mode={props.displayType !== 'console' ? mode : 'html'}
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
fontSize={14}
showGutter={ props.displayType !== 'console'}
onChange={onChangeText}
//onLoad={onLoad}
highlightActiveLine={false}
showPrintMargin={false}
value={plaintextView}
height={props.expand ? "100%": undefined}
maxLines={props.expand ? undefined : 20}
width={"100%"}
//style={{backgroundColor: "transparent"}}
//autoScrollEditorIntoView={true}
wrapEnabled={props.displayType !== 'console' ? wrapText : true}
minLines={1}
setOptions={{
showLineNumbers: props.displayType !== 'console',
tabSize: 4,
useWorker: false,
showInvisibles: false,
}}/>
}
</div>
</div>
)
@@ -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>
</>
@@ -18,45 +18,46 @@ export function ResponseDisplayScreenshotModal({onClose, images, startIndex}) {
const handleNext = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};
return (
<React.Fragment>
<DialogContent dividers={true} style={{padding: 0}}>
<div style={{height: "calc(80vh)", display: zoom ? "" : "flex", alignItems: zoom ? "" : "center" , background: theme.palette.mode === "dark" ? "rgb(44, 52, 60)" : "grey"}}>
<img onClick={toggleZoom} src={"/api/v1.4/files/screencaptures/" + images[activeStep]} style={{width: zoom ? "" : "100%", cursor: zoom ? "zoom-out" : "zoom-in", overflow: "auto"}} />
< >
<DialogContent dividers={true} style={{padding: 0}} >
<div style={{height: "calc(80vh)", display: zoom ? "" : "flex", alignItems: zoom ? "" : "flex-start" , background: theme.palette.mode === "dark" ? "rgb(44, 52, 60)" : "grey"}}>
<img onClick={toggleZoom} src={"/api/v1.4/files/screencaptures/" + images[activeStep]}
style={{width: zoom ? "" : "100%", cursor: zoom ? "zoom-out" : "zoom-in", overflow: "auto"}} />
</div>
</DialogContent>
<MobileStepper
variant="text"
steps={maxSteps}
position="static"
activeStep={activeStep}
nextButton={
<Button
size="small"
onClick={handleNext}
disabled={activeStep === maxSteps - 1}
>
Next
{<KeyboardArrowRight />}
</Button>
}
backButton={
<Button size="small" onClick={handleBack} disabled={activeStep === 0}>
{<KeyboardArrowLeft />}
Back
</Button>
}
variant="text"
steps={maxSteps}
position="static"
activeStep={activeStep}
nextButton={
<Button
size="small"
onClick={handleNext}
disabled={activeStep === maxSteps - 1}
>
Next
{<KeyboardArrowRight />}
</Button>
}
backButton={
<Button size="small" onClick={handleBack} disabled={activeStep === 0}>
{<KeyboardArrowLeft />}
Back
</Button>
}
/>
<DialogActions>
<DialogActions >
<Button variant="contained" onClick={onClose} color="primary">
Close
</Button>
</DialogActions>
</React.Fragment>
</>
);
}
@@ -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';
@@ -8,10 +8,6 @@ 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 IconButton from '@mui/material/IconButton';
@@ -19,7 +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);
@@ -31,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;
@@ -42,6 +52,7 @@ export const getIconName = (iconName) => {
case "diskimage":
return faBoxOpen;
case "executable":
case "cog":
return faCog;
case "word":
return faFileWord;
@@ -78,51 +89,82 @@ 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 ResponseDisplayTableStringCell = ({cellData, rowData}) => {
const ResponseDisplayTableStringCellCopy = ({cellData}) => {
return (
<div style={{...(cellData?.cellStyle || null)}}>
{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
}
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%"}}>
<ResponseDisplayTableStringCellCopy cellData={cellData} />
<ResponseDisplayTableFontAwesomeStartIcon cellData={cellData} />
{cellData?.plaintextHoverText? (
<MythicStyledTooltip title={cellData.plaintextHoverText}>
<pre style={{display: "inline-block"}}>
<pre style={{display: "inline-block", margin: 0}}>
{cellData?.plaintext?.replaceAll?.("\n", "") || " "}
</pre>
</MythicStyledTooltip>
) : (
<pre style={{display: "inline-block"}}>
<pre style={{display: "inline-block", margin: 0}}>
{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>
);
}
const ResponseDisplayTableNumberCell = ({cellData, rowData}) => {
return (
<div style={{...(cellData?.cellStyle || null)}}>
<div style={{...(cellData?.cellStyle || null), height: "100%"}}>
{cellData?.copyIcon?
<MythicStyledTooltip title={"Copy to clipboard"}>
<IconButton onClick={() => onCopyToClipboard(cellData["plaintext"])} size="small">
@@ -137,13 +179,13 @@ const ResponseDisplayTableNumberCell = ({cellData, rowData}) => {
}
{cellData?.plaintextHoverText? (
<MythicStyledTooltip title={cellData.plaintextHoverText}>
<pre style={{display: "inline-block"}}>
<pre style={{display: "inline-block", margin: 0}}>
{cellData?.plaintext || " "}
</pre>
</MythicStyledTooltip>
) : (
<pre style={{display: "inline-block"}}>
<pre style={{display: "inline-block", margin: 0}}>
{cellData?.plaintext || " "}
</pre>
)}
@@ -179,23 +221,32 @@ export const getStringSize = ({cellData}) => {
}
const ResponseDisplayTableSizeCell = ({cellData, rowData}) => {
return (
<div style={{...(cellData?.cellStyle || null)}}>
<div style={{...(cellData?.cellStyle || null), height: "100%"}}>
{cellData?.plaintextHoverText? (
<MythicStyledTooltip title={cellData.plaintextHoverText} >
<pre style={{display: "inline-block"}}>
<pre style={{display: "inline-block", margin: 0}}>
{getStringSize({cellData})}
</pre>
</MythicStyledTooltip>
) : (
<pre style={{display: "inline-block"}}>
<pre style={{display: "inline-block", margin: 0}}>
{getStringSize({cellData})}
</pre>
)}
</div>
);
}
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);
@@ -248,191 +299,266 @@ 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 || " "} >
<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>
<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 || " "} >
<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>
<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 || " "} >
<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>
<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 || " "}>
<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>
<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 || "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} />}
/>
<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" 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>
{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)}}>
{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 = 35;
const headerHeight = 45;
const rowHeight = GetComputedFontSize() + 7;
const headerHeight = GetComputedFontSize() + 32;
const maxHeight = 375;
const [dataHeight, setDataHeight] = React.useState(maxHeight);
const maxElements = Math.floor(maxHeight / rowHeight);
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) => (parseInt(a[sortData.sortKey]["plaintext"]) > parseInt(b[sortData.sortKey]["plaintext"]) ? 1 : -1));
tmpData.sort((a, b) => {
if(a[sortData.sortKey]["plaintext"] === b[sortData.sortKey]["plaintext"]){
return 0;
}else if(a[sortData.sortKey]["plaintext"] === undefined || a[sortData.sortKey]["plaintext"] === null){
return -1;
}else if(b[sortData.sortKey]["plaintext"] === undefined || b[sortData.sortKey]["plaintext"] === null){
return 1;
}else{
try{
return parseInt(a[sortData.sortKey]["plaintext"]) > parseInt(b[sortData.sortKey]["plaintext"]) ? 1 : -1;
}catch(error){
console.log("failed to parse ints for sorting", a[sortData.sortKey]["plaintext"], b[sortData.sortKey]["plaintext"]);
return a[sortData.sortKey]["plaintext"] > b[sortData.sortKey]["plaintext"] ? 1: -1;
}
}
});
}else if(sortData.sortType === "date"){
tmpData.sort((a,b) => ( (new Date(a[sortData.sortKey]["plaintext"])) > (new Date(b[sortData.sortKey]["plaintext"])) ? 1: -1));
tmpData.sort((a,b) => {
if(a[sortData.sortKey]["plaintext"] === b[sortData.sortKey]["plaintext"]){
return 0;
}else if(a[sortData.sortKey]["plaintext"] === undefined || a[sortData.sortKey]["plaintext"] === null){
return -1;
}else if(b[sortData.sortKey]["plaintext"] === undefined || b[sortData.sortKey]["plaintext"] === null){
return 1;
}else{
try{
return (new Date(a[sortData.sortKey]["plaintext"])) > (new Date(b[sortData.sortKey]["plaintext"])) ? 1: -1
}catch(error){
console.log("failed to parse dates for sorting", a[sortData.sortKey]["plaintext"], b[sortData.sortKey]["plaintext"]);
return a[sortData.sortKey]["plaintext"] > b[sortData.sortKey]["plaintext"] ? 1: -1;
}
}
});
}else{
tmpData.sort( (a, b) => {
if(a[sortData.sortKey]["plaintext"] === undefined){
if(a[sortData.sortKey]["plaintext"] === b[sortData.sortKey]["plaintext"]){
return 0;
}else if(a[sortData.sortKey]["plaintext"] === undefined || a[sortData.sortKey]["plaintext"] === null){
return -1;
}else if(b[sortData.sortKey]["plaintext"] === undefined){
}else if(b[sortData.sortKey]["plaintext"] === undefined || b[sortData.sortKey]["plaintext"] === null){
return 1;
} else {
try{
return a[sortData.sortKey]["plaintext"].localeCompare(b[sortData.sortKey]["plaintext"]);
}catch(error){
console.log("failed to localeCompare strings for sorting", a[sortData.sortKey]["plaintext"], b[sortData.sortKey]["plaintext"]);
return a[sortData.sortKey]["plaintext"] > b[sortData.sortKey]["plaintext"] ? 1: -1;
}
}
return a[sortData.sortKey]["plaintext"].toLowerCase() > b[sortData.sortKey]["plaintext"].toLowerCase() ? 1 : -1
});
}
if(sortData.sortDirection === "DESC"){
tmpData.reverse();
}
return tmpData;
}, [allData, sortData]);
}, [allData, sortData, filterOptions]);
const onClickHeader = (e, columnIndex) => {
const column = table.headers[columnIndex];
@@ -444,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"});
@@ -454,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)){
@@ -498,16 +598,16 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
}
return rowData;
}
const contextMenuOptions = [
const rowContextMenuOptions = [
{
name: 'Copy Row as JSON',
name: 'Copy Row as JSON', icon: null,
click: ({event, columnIndex, rowIndex, data}) => {
const filteredData = filterOutButtonsFromRowData(data);
onCopyToClipboard(JSON.stringify(filteredData, null, 2));
}
}, type: "item"
},
{
name: 'Copy Row as CSV',
name: 'Copy Row as CSV', icon: null,
click: ({event, columnIndex, rowIndex, data}) => {
const filteredData = filterOutButtonsFromRowData(data);
let outputHeaders = "";
@@ -525,10 +625,11 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
}
}
onCopyToClipboard(outputHeaders + "\n" + outputRow);
}
},
type: "item",
},
{
name: 'Copy Row as TSV',
{
name: 'Copy Row as TSV', icon: null,
click: ({event, columnIndex, rowIndex, data}) => {
const filteredData = filterOutButtonsFromRowData(data);
let outputHeaders = "";
@@ -546,43 +647,176 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
}
}
onCopyToClipboard(outputHeaders + "\n" + outputRow);
}
},
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%",} : {height: dataHeight}
}, [expand, dataHeight]);
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 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}
@@ -591,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,11 +1,7 @@
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";
const updateCommentMutation = gql`
mutation updateComment ($task_id: Int!, $comment: String) {
@@ -42,6 +38,7 @@ export function TaskCommentDialog(props) {
const [updateComment] = useMutation(updateCommentMutation, {
update: (cache, {data}) => {
//console.log(data);
props.onClose();
}
});
if (loading) {
@@ -51,28 +48,17 @@ export function TaskCommentDialog(props) {
console.error(error);
return <div>Error!</div>;
}
const onCommitSubmit = () => {
updateComment({variables: {task_id: props.task_id, comment: comment}});
props.onClose();
const onCommitSubmit = (newValue) => {
updateComment({variables: {task_id: props.task_id, comment: newValue}});
}
const onChange = (name, value, error) => {
setComment(value);
}
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Edit Task Comment</DialogTitle>
<DialogContent dividers={true}>
<MythicTextField autoFocus onEnter={onCommitSubmit} onChange={onChange} value={comment} />
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
Close
</Button>
<Button onClick={onCommitSubmit} variant="contained" color="success">
Submit
</Button>
</DialogActions>
<MythicModifyStringDialog title={`Edit Task Comment`}
onClose={props.onClose}
value={comment}
dontCloseOnSubmit={true}
onSubmit={onCommitSubmit} />
</React.Fragment>
);
}
File diff suppressed because it is too large. Load diff
@@ -27,7 +27,6 @@ import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons';
import { faExternalLinkAlt, faExpandArrowsAlt } from '@fortawesome/free-solid-svg-icons';
import SearchIcon from '@mui/icons-material/Search';
import SpeedDial from '@mui/material/SpeedDial';
import SpeedDialIcon from '@mui/material/SpeedDialIcon';
import SpeedDialAction from '@mui/material/SpeedDialAction';
import { Backdrop } from '@mui/material';
import {downloadFileFromMemory} from '../../utilities/Clipboard';
@@ -36,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!){
@@ -69,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]);
@@ -90,11 +95,12 @@ export const TaskDisplayContainer = ({task, me}) => {
me={me}
responseRef={responseRef}
viewBrowserScript={viewBrowserScript}
style={{position: "relative", zIndex: 2, display: "flex", flexDirection: "row-reverse", width: '100%', bottom: "20px", right: "5px", height: 0}}
style={{position: "relative", zIndex: 2, display: "flex", flexDirection: "row-reverse", width: '100%',
bottom: "25px", right: "4px", height: 0}}
fabStyle={{ }}
viewAllOutput={selectAllOutput}/>
<Grid item xs={12}>
<ResponseDisplay
<Grid size={12}>
<ResponseDisplay
task={task}
me={me}
command_id={commandID}
@@ -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,8 +136,9 @@ 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}
me={me}
command_id={commandID}
@@ -141,13 +148,13 @@ export const TaskDisplayContainerFlat = ({task, me}) => {
expand={true}
responseRef={responseRef}
/>
<SpeedDialDisplayGeneric toggleViewBrowserScript={toggleViewBrowserScript}
<SideDisplayGeneric toggleViewBrowserScript={toggleViewBrowserScript}
toggleSelectAllOutput={toggleSelectAllOutput}
toggleOpenSearch={toggleOpenSearch}
taskData={task}
viewBrowserScript={viewBrowserScript}
me={me} responseRef={responseRef}
style={{position: "absolute", bottom: 0, zIndex: 2}}
style={{position: "absolute", bottom: "15px", zIndex: 2}}
fabStyle={{ }}
viewAllOutput={selectAllOutput}/>
</div>
@@ -156,35 +163,15 @@ export const TaskDisplayContainerFlat = ({task, me}) => {
}
export const TaskDisplayContainerConsole = ({task, me}) => {
const [viewBrowserScript, setViewBrowserScript] = React.useState(true);
const [commandID, setCommandID] = React.useState(0);
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: "20px", right: "5px", zIndex: 2, height: "0px", position: "relative", display: "flex", flexDirection: "row-reverse"}}
fabStyle={{ }}
viewAllOutput={true}/>
<ResponseDisplayConsole
task={task}
me={me}
@@ -197,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);
@@ -241,7 +483,8 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput
setOpenSpeedDial(false);
};
const copyToClipboard = () => {
let result = copyStringToClipboard(task.original_params);
let command = task?.command?.cmd || task.command_name;
let result = copyStringToClipboard(command + " " + task.original_params);
if(result){
snackActions.success("Copied text!");
}else{
@@ -304,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="md" 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="md" 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="md" 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="md" 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"} />}
@@ -419,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
@@ -429,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}}/>}
@@ -38,6 +38,9 @@ const getLoadedCommandsBasedOnInput = ({cmd, ui_feature}) => {
id
needs_admin
payload_type_id
payloadtype {
name
}
attributes
commandparameters {
id
@@ -128,7 +131,7 @@ export const TaskFromUIButton = ({callback_id, callback_ids, cmd, ui_feature, pa
onTasked({tasked: false});
return;
}
const availableCommands = data.callback_by_pk.loadedcommands.reduce( (prev, cur) => {
let availableCommands = data.callback_by_pk.loadedcommands.reduce( (prev, cur) => {
if(typeof(parameters) === "string"){
return [...prev, {...cur.command, "parsedParameters": {}}];
}else{
@@ -141,6 +144,7 @@ export const TaskFromUIButton = ({callback_id, callback_ids, cmd, ui_feature, pa
return [...prev, {...cur.token, "display": renderValue(cur.token)}]
}, []);
setCallbackTokenOptions(availableTokens);
availableCommands = availableCommands.map(c => {return {...c, display: `${c.cmd} ( ${c.payloadtype.name} )`}});
setFileBrowserCommands(availableCommands);
if(availableCommands.length === 0){
if(ui_feature !== undefined){
@@ -226,7 +230,7 @@ export const TaskFromUIButton = ({callback_id, callback_ids, cmd, ui_feature, pa
createTask({variables: {...variables, callback_id: callbackData.callback_by_pk.display_id}})
}
}
const submitParametersDialog = (cmd, new_parameters, files) => {
const submitParametersDialog = (cmd, new_parameters, files, selectedParameterGroup, payload_type) => {
setOpenParametersDialog(false);
try{
savedFinalVariables.current = JSON.parse(new_parameters);
@@ -238,7 +242,15 @@ export const TaskFromUIButton = ({callback_id, callback_ids, cmd, ui_feature, pa
savedFinalVariables.current = new_parameters;
}
onSubmitTasking({variables: {callback_id: callbackData.callback_by_pk.display_id, command: cmd, params: new_parameters, files, tasking_location: "modal"}});
onSubmitTasking({variables: {
callback_id: callbackData.callback_by_pk.display_id,
command: cmd,
params: new_parameters,
files,
tasking_location: "modal",
payload_type: payload_type,
parameter_group_name: selectedParameterGroup
}});
}
const onSubmitSelectedToken = (token) => {
setSelectedCallbackToken(token);
@@ -304,22 +316,46 @@ export const TaskFromUIButton = ({callback_id, callback_ids, cmd, ui_feature, pa
}else{
savedFinalVariables.current = parameters;
if(typeof(parameters) === "string"){
onSubmitTasking({variables: {callback_id: callbackData.callback_by_pk.display_id, command: selectedCommand.cmd, params: parameters, tasking_location: "command_line"}});
onSubmitTasking({variables: {
callback_id: callbackData.callback_by_pk.display_id,
command: selectedCommand.cmd,
params: parameters,
payload_type: selectedCommand?.payloadtype?.name,
tasking_location: "command_line"}});
}else{
onSubmitTasking({variables: {callback_id: callbackData.callback_by_pk.display_id, command: selectedCommand.cmd, params: JSON.stringify(parameters), tasking_location: taskingLocation}});
onSubmitTasking({variables: {
callback_id: callbackData.callback_by_pk.display_id,
command: selectedCommand.cmd,
params: JSON.stringify(parameters),
payload_type: selectedCommand?.payloadtype?.name,
tasking_location: taskingLocation}});
}
}
}else{
if(parameters === undefined || parameters === null){
savedFinalVariables.current = "";
onSubmitTasking({variables: {callback_id: callbackData.callback_by_pk.display_id, command: selectedCommand.cmd, params: ""}});
onSubmitTasking({variables: {
callback_id: callbackData.callback_by_pk.display_id,
command: selectedCommand.cmd,
payload_type: selectedCommand?.payloadtype?.name,
params: ""}});
}else{
savedFinalVariables.current = parameters;
if(typeof(parameters) === "string"){
onSubmitTasking({variables: {callback_id: callbackData.callback_by_pk.display_id, command: selectedCommand.cmd, params: parameters, tasking_location: "command_line"}});
onSubmitTasking({variables: {
callback_id: callbackData.callback_by_pk.display_id,
command: selectedCommand.cmd,
payload_type: selectedCommand?.payloadtype?.name,
params: parameters,
tasking_location: "command_line"}});
}else{
onSubmitTasking({variables: {callback_id: callbackData.callback_by_pk.display_id, command: selectedCommand.cmd, params: JSON.stringify(parameters), tasking_location: taskingLocation}});
onSubmitTasking({variables: {
callback_id: callbackData.callback_by_pk.display_id,
command: selectedCommand.cmd,
payload_type: selectedCommand?.payloadtype?.name,
params: JSON.stringify(parameters),
tasking_location: taskingLocation}});
}
}
@@ -332,7 +368,7 @@ export const TaskFromUIButton = ({callback_id, callback_ids, cmd, ui_feature, pa
onClose={()=>{setOpenSelectCommandDialog(false);onTasked({tasked: false});}}
innerDialog={<MythicSelectFromListDialog onClose={()=>{setOpenSelectCommandDialog(false);onTasked({tasked: false});}}
onSubmit={onSubmitSelectedCommand} options={fileBrowserCommands} title={"Select Command"}
action={"select"} identifier={"id"} display={"cmd"} dontCloseOnSubmit={true} />}
action={"select"} identifier={"id"} display={"display"} dontCloseOnSubmit={true} />}
/>
}
{openSelectCallback &&
@@ -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>
);
}
@@ -10,7 +10,6 @@ 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 {TaskParametersDialogRow} from './TaskParametersDialogRow';
import {gql, useLazyQuery, useMutation, useQuery} from '@apollo/client';
import FormControl from '@mui/material/FormControl';
@@ -20,9 +19,9 @@ import Input from '@mui/material/Input';
import {UploadTaskFile} from '../../MythicComponents/MythicFileUpload';
import {Backdrop, CircularProgress} from '@mui/material';
import Divider from '@mui/material/Divider';
import {useTheme} from '@mui/material/styles';
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`
@@ -107,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
@@ -126,6 +125,7 @@ query getAllPayloadsQuery($operation_id: Int!){
filemetum {
id
filename_text
timestamp
}
buildparameterinstances {
value
@@ -142,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 {
@@ -243,6 +243,7 @@ query getCommandQuery($id: Int!){
choices
choices_are_all_commands
choices_are_loaded_commands
limit_credentials_by_type
default_value
description
id
@@ -256,6 +257,7 @@ query getCommandQuery($id: Int!){
parameter_group_name
display_name
cli_name
verifier_regex
}
}
}
@@ -263,7 +265,7 @@ query getCommandQuery($id: Int!){
// use this to get all the credentials for the command we're trying to execute
const getCredentialsQuery = gql`
query getCredentialsQuery($operation_id: Int!){
credential(where: {deleted: {_eq: false}, operation_id: {_eq: $operation_id}}){
credential(where: {deleted: {_eq: false}, operation_id: {_eq: $operation_id}}, order_by: {id: desc}){
account
comment
credential_text
@@ -287,7 +289,6 @@ export const commandInParsedParameters = (cmd, parsedParameters) =>{
return undefined
}
export function TaskParametersDialog(props) {
const theme = useTheme();
const [backdropOpen, setBackdropOpen] = React.useState(false);
const [commandInfo, setCommandInfo] = useState({});
const [parameterGroups, setParameterGroups] = useState([]);
@@ -384,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);
@@ -484,11 +484,17 @@ export function TaskParametersDialog(props) {
if(parsedParameterName){
return [...prev, {...cmd, value: props.command.parsedParameters[parsedParameterName]}];
}else if(cmd.default_value.length > 0){
return [...prev, {...cmd, value: JSON.parse(cmd.default_value)}];
try {
return [...prev, {...cmd, value: JSON.parse(cmd.default_value)}];
}catch(error){
return [...prev, {...cmd, value: [[cmd.default_value, ""]] }];
}
}else{
return [...prev, {...cmd, value: []}];
}
case "ChooseOne":
case "ChooseOneCustom":
case "ChooseMultiple":
let choices = cmd.choices;
let defaultV = cmd.default_value;
@@ -511,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;
}
}
@@ -535,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;
}
}
@@ -553,21 +575,35 @@ 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}];
}
case "File":
return [...prev, {...cmd, value: {} }];
return [...prev, {...cmd, value: {} }];
case "FileMultiple":
return [...prev, {...cmd, value: []}];
case "CredentialJson":
if (loadedCredentialsLoading.credential.length > 0){
let credentialChoices = loadedCredentialsLoading.credential;
if(credentialChoices === undefined || credentialChoices === null){
credentialChoices = [];
}
if(cmd.limit_credentials_by_type?.length > 0){
credentialChoices = credentialChoices.reduce( (existingCreds, curCred) => {
if(cmd.limit_credentials_by_type.includes(curCred.type)){
return [...existingCreds, curCred];
}
return [...existingCreds];
}, []);
}
if (credentialChoices.length > 0){
if(parsedParameterName){
cmd.value = props.command.parsedParameters[parsedParameterName];
}
else if(cmd.value === "" || (typeof(cmd.value) === Object && Object.keys(cmd.value).length === 0) || cmd.value === undefined){
cmd.value = loadedCredentialsLoading.credential[0];
cmd.value = credentialChoices[0];
}
return [...prev, {...cmd, choices: loadedCredentialsLoading.credential}];
return [...prev, {...cmd, choices: credentialChoices}];
}else{
return [...prev, {...cmd, value: {}, choices: []}];
}
@@ -586,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) => {
@@ -754,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){
@@ -816,6 +846,7 @@ export function TaskParametersDialog(props) {
case "Boolean":
case "Number":
case "ChooseOne":
case "ChooseOneCustom":
case "ChooseMultiple":
case "PayloadList":
case "Array":
@@ -832,16 +863,37 @@ export function TaskParametersDialog(props) {
collapsedParameters[param.name] = param.value;
break
case "File":
setBackdropOpen(true);
const newUUID = await UploadTaskFile(param.value, "Uploaded as part of tasking");
if(newUUID){
newFileUUIDs.push(newUUID);
collapsedParameters[param.name] = newUUID;
if(newUUID !== "Missing file in form"){
newFileUUIDs.push(newUUID);
collapsedParameters[param.name] = newUUID;
}
}else{
setBackdropOpen(false);
return;
}
break;
case "FileMultiple":
let fileIDs = [];
for(let i = 0; i < param.value.length; i++){
if(typeof param.value[i] === "string"){
fileIDs.push(param.value[i]);
continue
}
const newUUID = await UploadTaskFile(param.value[i], "Uploaded as part of tasking");
if(newUUID){
if(newUUID !== "Missing file in form"){
newFileUUIDs.push(newUUID);
fileIDs.push(newUUID);
} else {
snackActions.warning("Failed to upload file");
}
} else {
snackActions.warning("Failed to upload file");
}
}
collapsedParameters[param.name] = fileIDs;
break;
case "CredentialJson":
collapsedParameters[param.name] = {
account: param.value["account"],
@@ -856,7 +908,7 @@ export function TaskParametersDialog(props) {
}
}
setBackdropOpen(false);
props.onSubmit(props.command.cmd, JSON.stringify(collapsedParameters), newFileUUIDs, selectedParameterGroup);
props.onSubmit(commandInfo.cmd, JSON.stringify(collapsedParameters), newFileUUIDs, selectedParameterGroup, commandInfo?.payloadtype?.name);
}
const onAgentConnectAddNewPayloadOnHost = (host, payload) => {
@@ -881,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" />
@@ -930,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,11 +89,8 @@ export function TaskTokenDialog(props) {
});
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Token Information</DialogTitle>
<Paper elevation={5} style={{position: "relative"}} variant={"elevation"}>
<TableContainer component={Paper} className="mythicElement">
<MythicDraggableDialogTitle >Token Information</MythicDraggableDialogTitle>
<TableContainer className="mythicElement">
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
<TableHead>
<TableRow>
@@ -111,7 +108,6 @@ export function TaskTokenDialog(props) {
</TableBody>
</Table>
</TableContainer>
</Paper>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
Close
@@ -1,30 +1,32 @@
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
payloadtype {
name
id
}
}
}
}
@@ -32,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;
@@ -46,7 +49,13 @@ export function TaskViewParametersDialog(props) {
workingComment += "\n\nOriginal Command: " + data.task_by_pk.command.cmd;
workingComment += "\nIssued Command: " + data.task_by_pk.command_name;
}
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"
@@ -59,30 +68,14 @@ export function TaskViewParametersDialog(props) {
return <div>Error!</div>;
}
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">View Task Parameters</DialogTitle>
<DialogContent dividers={true}>
<AceEditor
mode="json"
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
fontSize={14}
showGutter={true}
highlightActiveLine={true}
value={comment}
focus={true}
width={"100%"}
setOptions={{
showLineNumbers: true,
useWorker: false,
tabSize: 4
}}/>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
Close
</Button>
</DialogActions>
</React.Fragment>
<>
<MythicModifyStringDialog title={`View Task Parameters And Timestamps`}
onClose={props.onClose}
maxRows={40}
wrap={true}
value={comment} />
</>
);
}
@@ -1,16 +1,8 @@
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";
const getParametersQuery = gql`
query getParametersQuery ($task_id: Int!) {
@@ -24,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 => {
@@ -41,28 +32,11 @@ export function TaskViewStdoutStderrDialog(props) {
}
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">View Task Stdout/Stderr</DialogTitle>
<DialogContent dividers={true}>
<AceEditor
mode="json"
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
fontSize={14}
showGutter={true}
highlightActiveLine={true}
value={comment}
focus={true}
width={"100%"}
setOptions={{
showLineNumbers: true,
tabSize: 4,
useWorker: false,
}}/>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
Close
</Button>
</DialogActions>
<MythicModifyStringDialog title={`View Task Stdout/Stderr`}
onClose={props.onClose}
maxRows={40}
wrap={true}
value={comment} />
</React.Fragment>
);
}
Loaded 100 of 660 files, more files were not shown because too many files have changed in this diff. Show more