mirror of
https://github.com/its-a-feature/Mythic
synced 2026-06-08 14:55:38 +00:00
custom browser update
This commit is contained in:
@@ -4,6 +4,37 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [3.4.11] - 2025-12-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Added `CustomBrowser` containers to allow customized file-like browser creation
|
||||
- This includes an "export" functionality that's a custom function you can implement per browser type
|
||||
- This includes a new field in `responses` for `custom_browser`
|
||||
- Added `SendMythicRPCCallbackTokenSearch`
|
||||
- Added `SendMythicRPCCustomBrowserSearch`
|
||||
- Added `verifier_regex` option to CommandParameters
|
||||
- Allows you to provide regex that must match the supplied value
|
||||
- Changed `mythictree` to move `has_children` out of metadata in support of CustomBrowsers
|
||||
- Changed `mythictree` to have new `display_path` field separate from `full_path` in support of CustomBrowsers
|
||||
- Added field to `payload` to track the version of the backing payload_type when the payload was created
|
||||
- This makes it easier to track payloads as you're doing payload type development/updates
|
||||
- Added `supported_c2` and `supported_wrapping` to Payload Types to track supported C2/Wrappers
|
||||
- This is now used to "resync" data rather than asking the container to resync back to Mythic
|
||||
- Changed `token_id` to int64 (from uint64) to match similar change in Postgres database to BigInt from just Integer
|
||||
- Updated operation event log auto resolver to use new `warining=true` vs `level='warning'` check
|
||||
- Added a 5s sleep between container's OnStart messages to help with multiple operations bombarding a container in rapid succession
|
||||
- Updated standard RPC timeouts to 10s instead of 5s
|
||||
- Updated auto re-hosting of files to have a 10s delay between files
|
||||
- Updated ContainerVersion out of bounds message to be a bit clearer
|
||||
- Updated `SendMythicRPCCallbackUpdate` to allow specifying the `dead` attribute
|
||||
- Updated `SendMythicRPCFileSearch` to allow specifying a PayloadUUID in addition to AgentFileID in `AgentFileID` field
|
||||
- Updated `SendMythicRPCResponseCreate` to go through the same process as an agent's `user_output`
|
||||
- This allows responses in this way to be intercepted and emitted to logging
|
||||
- Added some mutexes when syncing PayloadTypes to prevent possible race conditions
|
||||
- Updated file downloads to allow specifying a negative `total_chunks` until you're done and know how many chunks total there are
|
||||
- Added new `database` tab to file previews to run SQL queries on database right in the browser
|
||||
|
||||
## [3.4.10] - 2025-10-31
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -130,4 +130,49 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
SOFTWARE.
|
||||
|
||||
MIT license
|
||||
===========
|
||||
|
||||
Copyright (c) 2017 sql.js authors (see AUTHORS)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
|
||||
# Some portions of the Makefile taken from:
|
||||
Copyright 2017 Ryusei Yamaguchi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,26 @@
|
||||
const webpack = require('webpack');
|
||||
module.exports = function override(config, env) {
|
||||
config.resolve.fallback = {
|
||||
//url: require.resolve('url'),
|
||||
fs: false,
|
||||
assert: require.resolve('assert'),
|
||||
crypto: require.resolve('crypto-browserify'),
|
||||
path: require.resolve('path-browserify'),
|
||||
//http: require.resolve('stream-http'),
|
||||
//https: require.resolve('https-browserify'),
|
||||
//os: require.resolve('os-browserify/browser'),
|
||||
//buffer: require.resolve('buffer'),
|
||||
stream: require.resolve('stream-browserify'),
|
||||
};
|
||||
/*
|
||||
config.plugins.push(
|
||||
new webpack.ProvidePlugin({
|
||||
process: 'process/browser',
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
);
|
||||
|
||||
*/
|
||||
|
||||
return config;
|
||||
}
|
||||
Generated
+697
-1
@@ -37,6 +37,7 @@
|
||||
"jwt-decode": "^4.0.0",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"path-browserify": "^1.0.1",
|
||||
"randexp": "^0.5.3",
|
||||
"react": "^18.3.1",
|
||||
"react-ace": "^14.0.1",
|
||||
@@ -54,13 +55,18 @@
|
||||
"react-window": "^1.8.11",
|
||||
"save-svg-as-png": "^1.4.17",
|
||||
"semver": "^7.6.2",
|
||||
"sql.js": "^1.13.0",
|
||||
"styled-components": "^6.1.17",
|
||||
"uuid": "^9.0.1",
|
||||
"webpack": "^5.99.6",
|
||||
"word-wrap": "^1.2.5"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
@@ -6027,6 +6033,25 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asn1.js": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
|
||||
"integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bn.js": "^4.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asn1.js/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ast-types-flow": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
|
||||
@@ -6427,6 +6452,27 @@
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/batch": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
|
||||
@@ -6487,6 +6533,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bn.js": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz",
|
||||
"integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
@@ -6584,6 +6637,13 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/brorand": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
|
||||
"integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/browser-process-hrtime": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
|
||||
@@ -6591,6 +6651,129 @@
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/browserify-aes": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
|
||||
"integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-xor": "^1.0.3",
|
||||
"cipher-base": "^1.0.0",
|
||||
"create-hash": "^1.1.0",
|
||||
"evp_bytestokey": "^1.0.3",
|
||||
"inherits": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/browserify-cipher": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz",
|
||||
"integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserify-aes": "^1.0.4",
|
||||
"browserify-des": "^1.0.0",
|
||||
"evp_bytestokey": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/browserify-des": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz",
|
||||
"integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cipher-base": "^1.0.1",
|
||||
"des.js": "^1.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"safe-buffer": "^5.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/browserify-rsa": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz",
|
||||
"integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bn.js": "^5.2.1",
|
||||
"randombytes": "^2.1.0",
|
||||
"safe-buffer": "^5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/browserify-sign": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.5.tgz",
|
||||
"integrity": "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bn.js": "^5.2.2",
|
||||
"browserify-rsa": "^4.1.1",
|
||||
"create-hash": "^1.2.0",
|
||||
"create-hmac": "^1.1.7",
|
||||
"elliptic": "^6.6.1",
|
||||
"inherits": "^2.0.4",
|
||||
"parse-asn1": "^5.1.9",
|
||||
"readable-stream": "^2.3.8",
|
||||
"safe-buffer": "^5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/browserify-sign/node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/browserify-sign/node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/browserify-sign/node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
|
||||
@@ -6633,12 +6816,44 @@
|
||||
"node-int64": "^0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/buffer-xor": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
|
||||
"integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/builtin-modules": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
|
||||
@@ -6904,6 +7119,21 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cipher-base": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz",
|
||||
"integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.4",
|
||||
"safe-buffer": "^5.2.1",
|
||||
"to-buffer": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/cjs-module-lexer": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
|
||||
@@ -7331,6 +7561,53 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/create-ecdh": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
|
||||
"integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bn.js": "^4.1.0",
|
||||
"elliptic": "^6.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/create-ecdh/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/create-hash": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
|
||||
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cipher-base": "^1.0.1",
|
||||
"inherits": "^2.0.1",
|
||||
"md5.js": "^1.3.4",
|
||||
"ripemd160": "^2.0.1",
|
||||
"sha.js": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/create-hmac": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
|
||||
"integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cipher-base": "^1.0.3",
|
||||
"create-hash": "^1.1.0",
|
||||
"inherits": "^2.0.1",
|
||||
"ripemd160": "^2.0.0",
|
||||
"safe-buffer": "^5.0.1",
|
||||
"sha.js": "^2.4.8"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -7346,6 +7623,33 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/crypto-browserify": {
|
||||
"version": "3.12.1",
|
||||
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz",
|
||||
"integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserify-cipher": "^1.0.1",
|
||||
"browserify-sign": "^4.2.3",
|
||||
"create-ecdh": "^4.0.4",
|
||||
"create-hash": "^1.2.0",
|
||||
"create-hmac": "^1.1.7",
|
||||
"diffie-hellman": "^5.0.3",
|
||||
"hash-base": "~3.0.4",
|
||||
"inherits": "^2.0.4",
|
||||
"pbkdf2": "^3.1.2",
|
||||
"public-encrypt": "^4.0.3",
|
||||
"randombytes": "^2.1.0",
|
||||
"randomfill": "^1.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/crypto-random-string": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
|
||||
@@ -8210,6 +8514,17 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/des.js": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz",
|
||||
"integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/destroy": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||
@@ -8296,6 +8611,25 @@
|
||||
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/diffie-hellman": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
|
||||
"integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bn.js": "^4.1.0",
|
||||
"miller-rabin": "^4.0.0",
|
||||
"randombytes": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/diffie-hellman/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
@@ -8546,6 +8880,29 @@
|
||||
"integrity": "sha512-v/3r+3Bl2NMrWmVoRTMBtHtWvRISTix/s9EfnsfEWApNrsmNjqgqJOispCGg46BPwIFdkag3N/HYSxJczvCm6w==",
|
||||
"license": "EPL-2.0"
|
||||
},
|
||||
"node_modules/elliptic": {
|
||||
"version": "6.6.1",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz",
|
||||
"integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bn.js": "^4.11.9",
|
||||
"brorand": "^1.1.0",
|
||||
"hash.js": "^1.0.0",
|
||||
"hmac-drbg": "^1.0.1",
|
||||
"inherits": "^2.0.4",
|
||||
"minimalistic-assert": "^1.0.1",
|
||||
"minimalistic-crypto-utils": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/elliptic/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emittery": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz",
|
||||
@@ -9566,6 +9923,17 @@
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/evp_bytestokey": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
|
||||
"integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"md5.js": "^1.3.4",
|
||||
"safe-buffer": "^5.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/execa": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
|
||||
@@ -10719,6 +11087,31 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hash-base": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz",
|
||||
"integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.4",
|
||||
"safe-buffer": "^5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/hash.js": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
|
||||
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"minimalistic-assert": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@@ -10741,6 +11134,18 @@
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/hmac-drbg": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
|
||||
"integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hash.js": "^1.0.3",
|
||||
"minimalistic-assert": "^1.0.0",
|
||||
"minimalistic-crypto-utils": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/hoist-non-react-statics": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||
@@ -11106,6 +11511,27 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -13417,6 +13843,18 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/md5.js": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
|
||||
"integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hash-base": "^3.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"safe-buffer": "^5.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz",
|
||||
@@ -13503,6 +13941,27 @@
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/miller-rabin": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
|
||||
"integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bn.js": "^4.0.0",
|
||||
"brorand": "^1.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"miller-rabin": "bin/miller-rabin"
|
||||
}
|
||||
},
|
||||
"node_modules/miller-rabin/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
@@ -13575,6 +14034,13 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/minimalistic-crypto-utils": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
|
||||
"integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -14181,6 +14647,23 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-asn1": {
|
||||
"version": "5.1.9",
|
||||
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.9.tgz",
|
||||
"integrity": "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"asn1.js": "^4.10.1",
|
||||
"browserify-aes": "^1.2.0",
|
||||
"evp_bytestokey": "^1.0.3",
|
||||
"pbkdf2": "^3.1.5",
|
||||
"safe-buffer": "^5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-json": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
||||
@@ -14227,6 +14710,12 @@
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/path-browserify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
@@ -14303,6 +14792,24 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pbkdf2": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz",
|
||||
"integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"create-hash": "^1.2.0",
|
||||
"create-hmac": "^1.1.7",
|
||||
"ripemd160": "^2.0.3",
|
||||
"safe-buffer": "^5.2.1",
|
||||
"sha.js": "^2.4.12",
|
||||
"to-buffer": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
@@ -15977,6 +16484,28 @@
|
||||
"url": "https://github.com/sponsors/lupomontero"
|
||||
}
|
||||
},
|
||||
"node_modules/public-encrypt": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
|
||||
"integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bn.js": "^4.1.0",
|
||||
"browserify-rsa": "^4.0.0",
|
||||
"create-hash": "^1.1.0",
|
||||
"parse-asn1": "^5.0.0",
|
||||
"randombytes": "^2.0.1",
|
||||
"safe-buffer": "^5.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/public-encrypt/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -16081,6 +16610,17 @@
|
||||
"safe-buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/randomfill": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz",
|
||||
"integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"randombytes": "^2.0.5",
|
||||
"safe-buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
@@ -16174,6 +16714,32 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-app-rewired": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-app-rewired/-/react-app-rewired-2.2.1.tgz",
|
||||
"integrity": "sha512-uFQWTErXeLDrMzOJHKp0h8P1z0LV9HzPGsJ6adOtGlA/B9WfT6Shh4j2tLTTGlXOfiVx6w6iWpp7SOC5pvk+gA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^5.6.0"
|
||||
},
|
||||
"bin": {
|
||||
"react-app-rewired": "bin/index.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-scripts": ">=2.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/react-app-rewired/node_modules/semver": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
},
|
||||
"node_modules/react-colorful": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz",
|
||||
@@ -17077,6 +17643,83 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/ripemd160": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz",
|
||||
"integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hash-base": "^3.1.2",
|
||||
"inherits": "^2.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ripemd160/node_modules/hash-base": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz",
|
||||
"integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^2.3.8",
|
||||
"safe-buffer": "^5.2.1",
|
||||
"to-buffer": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ripemd160/node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ripemd160/node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ripemd160/node_modules/readable-stream/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ripemd160/node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ripemd160/node_modules/string_decoder/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/robust-predicates": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
|
||||
@@ -17639,6 +18282,27 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/sha.js": {
|
||||
"version": "2.4.12",
|
||||
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz",
|
||||
"integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==",
|
||||
"dev": true,
|
||||
"license": "(MIT AND BSD-3-Clause)",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.4",
|
||||
"safe-buffer": "^5.2.1",
|
||||
"to-buffer": "^1.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"sha.js": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/shallowequal": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
|
||||
@@ -17922,6 +18586,12 @@
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/sql.js": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.13.0.tgz",
|
||||
"integrity": "sha512-RJbVP1HRDlUUXahJ7VMTcu9Rm1Nzw+EBpoPr94vnbD4LwR715F3CcxE2G2k45PewcaZ57pjetYa+LoSJLAASgA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stable": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
|
||||
@@ -18078,6 +18748,17 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/stream-browserify": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz",
|
||||
"integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "~2.0.4",
|
||||
"readable-stream": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
@@ -19053,6 +19734,21 @@
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/to-buffer": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz",
|
||||
"integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"isarray": "^2.0.5",
|
||||
"safe-buffer": "^5.2.1",
|
||||
"typed-array-buffer": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
"author": "@its_a_feature_",
|
||||
"description": "React UI for Mythic C2 Framework",
|
||||
"devDependencies": {
|
||||
"react-scripts": "^5.0.1"
|
||||
"buffer": "^6.0.3",
|
||||
"crypto-browserify": "^3.12.1",
|
||||
"react-app-rewired": "^2.2.1",
|
||||
"react-scripts": "^5.0.1",
|
||||
"stream-browserify": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.13.8",
|
||||
@@ -38,6 +42,7 @@
|
||||
"jwt-decode": "^4.0.0",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"path-browserify": "^1.0.1",
|
||||
"randexp": "^0.5.3",
|
||||
"react": "^18.3.1",
|
||||
"react-ace": "^14.0.1",
|
||||
@@ -55,18 +60,19 @@
|
||||
"react-window": "^1.8.11",
|
||||
"save-svg-as-png": "^1.4.17",
|
||||
"semver": "^7.6.2",
|
||||
"sql.js": "^1.13.0",
|
||||
"styled-components": "^6.1.17",
|
||||
"uuid": "^9.0.1",
|
||||
"webpack": "^5.99.6",
|
||||
"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": [
|
||||
|
||||
@@ -27,6 +27,7 @@ export const operatorSettingDefaults = {
|
||||
hideUsernames: false,
|
||||
showIP: false,
|
||||
showHostname: false,
|
||||
showOPSECBypassUsername: false,
|
||||
showCallbackGroups: false,
|
||||
useDisplayParamsForCLIHistory: true,
|
||||
interactType: "interact",
|
||||
@@ -108,8 +109,12 @@ export const operatorSettingDefaults = {
|
||||
light: '#ffffff'
|
||||
},
|
||||
navBarColor: {
|
||||
dark: "#2e373c",
|
||||
light: "#3c4d67",
|
||||
dark: "#194573",
|
||||
light: "#3b606d",
|
||||
},
|
||||
navBarBottomColor: {
|
||||
dark: "#330814",
|
||||
light: "#283581",
|
||||
},
|
||||
taskPromptTextColor: {
|
||||
dark: '#bebebe',
|
||||
@@ -131,6 +136,10 @@ export const operatorSettingDefaults = {
|
||||
dark: "#2a5953",
|
||||
light: "#a7ce9d",
|
||||
},
|
||||
emptyFolderColor: {
|
||||
dark: '#bebebe',
|
||||
light: '#a6a5a5'
|
||||
},
|
||||
},
|
||||
}
|
||||
export const defaultShortcuts = [
|
||||
|
||||
@@ -38,6 +38,16 @@ 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';
|
||||
|
||||
// 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`
|
||||
@@ -132,6 +142,8 @@ export function App(props) {
|
||||
},
|
||||
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
|
||||
@@ -146,6 +158,8 @@ export function App(props) {
|
||||
preferences?.palette?.taskContextImpersonationColor?.light || operatorSettingDefaults.palette.taskContextImpersonationColor.light,
|
||||
taskContextExtraColor: themeMode === 'dark' ? preferences?.palette?.taskContextExtraColor?.dark || operatorSettingDefaults.palette.taskContextExtraColor.dark :
|
||||
preferences?.palette?.taskContextExtraColor?.light || operatorSettingDefaults.palette.taskContextExtraColor.light,
|
||||
emptyFolderColor: themeMode === 'dark' ? preferences?.palette?.emptyFolderColor?.dark || operatorSettingDefaults.palette.emptyFolderColor.dark :
|
||||
preferences?.palette?.emptyFolderColor?.light || operatorSettingDefaults.palette.emptyFolderColor.light,
|
||||
})
|
||||
}catch(error){
|
||||
console.log(error);
|
||||
@@ -223,6 +237,10 @@ export function App(props) {
|
||||
},
|
||||
topAppBarColor: themeMode === 'dark' ? operatorSettingDefaults.palette.navBarColor.dark :
|
||||
operatorSettingDefaults.palette.navBarColor.light,
|
||||
topAppBarBottomColor: themeMode === 'dark' ? operatorSettingDefaults.palette.navBarBottomColor.dark :
|
||||
operatorSettingDefaults.palette.navBarBottomColor.light,
|
||||
emptyFolderColor: themeMode === 'dark' ? operatorSettingDefaults.palette.emptyFolderColor.dark :
|
||||
operatorSettingDefaults.palette.emptyFolderColor.light,
|
||||
typography: {
|
||||
fontSize: 12,//operatorSettingDefaults?.fontSize,
|
||||
fontFamily: operatorSettingDefaults?.fontFamily
|
||||
@@ -308,7 +326,6 @@ export function App(props) {
|
||||
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);}}
|
||||
@@ -317,15 +334,16 @@ export function App(props) {
|
||||
/>
|
||||
}
|
||||
{me.loggedIn && me.user !== undefined && me.user !== null ? (
|
||||
<TopAppBarVertical me={me} theme={themeMode} toggleTheme={themeToggler} />
|
||||
<TopAppBarVertical me={me} toggleTheme={themeToggler} />
|
||||
) : null}
|
||||
<div style={{
|
||||
maxHeight: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: "100%"
|
||||
width: "100%",
|
||||
}}>
|
||||
<div style={{height: "5px", width: "100%", backgroundColor: "unset !important", background: `linear-gradient(25deg, ${theme.topAppBarColor}, ${theme.topAppBarBottomColor})`}}/>
|
||||
{me.loggedIn && me?.user?.current_operation_banner_text !== "" &&
|
||||
<Typography style={{
|
||||
backgroundColor: me?.user?.current_operation_banner_color,
|
||||
|
||||
@@ -27,14 +27,15 @@ export const MythicAgentSVGIcon = ({payload_type, style, is_p2p}) => {
|
||||
|
||||
)
|
||||
}
|
||||
export const MythicAgentSVGIconNoTooltip = ({payload_type, is_p2p, className}) => {
|
||||
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'}}/>
|
||||
<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",
|
||||
|
||||
@@ -239,7 +239,7 @@ const VirtualTreeRow = React.memo(({
|
||||
height: "15px",
|
||||
marginLeft: '3px',
|
||||
marginRight: '5px',
|
||||
color: item?.metadata?.has_children ? theme.folderColor : theme.palette.text.secondary,
|
||||
color: item?.has_children || item.success ? theme.folderColor : theme.emptyFolderColor,
|
||||
}}
|
||||
size={"lg"}
|
||||
onClick={handleOnClickButton} />
|
||||
@@ -250,14 +250,14 @@ const VirtualTreeRow = React.memo(({
|
||||
height: "15px",
|
||||
marginLeft: '3px',
|
||||
marginRight: '5px',
|
||||
color: item?.metadata?.has_children ? theme.folderColor : theme.palette.text.secondary, }}
|
||||
color: item?.has_children || item.success ? theme.folderColor : theme.emptyFolderColor, }}
|
||||
size={"lg"}
|
||||
icon={faFolder} onClick={handleOnClickButton} />
|
||||
)}
|
||||
<Typography
|
||||
style={{
|
||||
color:
|
||||
item?.metadata?.has_children ||
|
||||
item?.has_children ||
|
||||
item.success !== null
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary,
|
||||
|
||||
@@ -22,6 +22,8 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
[data, rowIndex]
|
||||
);
|
||||
const handleClick = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if(data.onRowClick){
|
||||
data.onRowClick({event, rowDataStatic: data.items[rowIndex][columnIndex]?.props?.rowData})
|
||||
}
|
||||
@@ -44,6 +46,8 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
}
|
||||
}
|
||||
const handleMenuItemClick = (event, clickOption) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
clickOption({event, columnIndex, rowIndex, data: data.items[rowIndex][columnIndex]?.props?.rowData || {}});
|
||||
setOpenContextMenu(false);
|
||||
};
|
||||
@@ -55,7 +59,8 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
const handleContextClick = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
if(item.disableFilterMenu){
|
||||
event.stopPropagation();
|
||||
if(item?.disableFilterMenu){
|
||||
return;
|
||||
}
|
||||
contextMenuLocationRef.current.x = event.clientX;
|
||||
@@ -90,14 +95,14 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
{item}
|
||||
</div>
|
||||
<ContextMenu dropdownAnchorRef={dropdownAnchorRef} contextMenuOptions={contextMenuOptions}
|
||||
disableFilterMenu={item.disableFilterMenu} openContextMenu={openContextMenu}
|
||||
disableFilterMenu={item?.disableFilterMenu} openContextMenu={openContextMenu}
|
||||
contextMenuLocationRef={contextMenuLocationRef}
|
||||
setOpenContextMenu={setOpenContextMenu} handleMenuItemClick={handleMenuItemClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const ContextMenu = ({openContextMenu, dropdownAnchorRef, contextMenuOptions, setOpenContextMenu, handleMenuItemClick, contextMenuLocationRef}) => {
|
||||
export const ContextMenu = ({openContextMenu, dropdownAnchorRef, contextMenuOptions, setOpenContextMenu, handleMenuItemClick, contextMenuLocationRef}) => {
|
||||
const handleClose = (event) => {
|
||||
//if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
|
||||
// return;
|
||||
|
||||
+4
-2
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
||||
import {classes} from './styles';
|
||||
import Draggable from 'react-draggable';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import DragHandleIcon from '@mui/icons-material/DragHandle';
|
||||
|
||||
const DraggableHandles = React.forwardRef(({ height, rowHeight, width, minColumnWidth, columnWidths, onStop }, ref) => {
|
||||
const [isDragging, setIsDragging] = useState(-1);
|
||||
@@ -40,13 +41,14 @@ const DraggableHandles = React.forwardRef(({ height, rowHeight, width, minColumn
|
||||
setIsDragging(-1);
|
||||
onStop(data.x, i);
|
||||
}}>
|
||||
<MoreVertIcon
|
||||
<DragHandleIcon
|
||||
ref={nodeRef}
|
||||
className={isDragging === i ? classes.draggableHandlesClickAreaSelected : classes.draggableHandlesClickArea}
|
||||
style={{
|
||||
left: leftOffset + columnWidths[i] - 1 - 7,
|
||||
rotate: isDragging === i ? "0deg" : "90deg",
|
||||
}}>
|
||||
</MoreVertIcon>
|
||||
</DragHandleIcon>
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -5,13 +5,7 @@ import {classes} from './styles';
|
||||
import React from 'react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faFilter} from '@fortawesome/free-solid-svg-icons';
|
||||
import Grow from '@mui/material/Grow';
|
||||
import Popper from '@mui/material/Popper';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import MenuList from '@mui/material/MenuList';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import {ContextMenu} from "./Cell";
|
||||
|
||||
const HeaderCell = ({
|
||||
onClick = () => {},
|
||||
@@ -23,9 +17,9 @@ const HeaderCell = ({
|
||||
VariableSizeGridProps: { style, rowIndex, columnIndex, data },
|
||||
}) => {
|
||||
const dropdownAnchorRef = React.useRef(null);
|
||||
const theme = useTheme();
|
||||
const item = data.items[rowIndex][columnIndex];
|
||||
const isFiltered = item?.filtered || false;
|
||||
const contextMenuLocationRef = React.useRef({x: 0, y: 0});
|
||||
const handleClick = useCallback(
|
||||
(e) => {
|
||||
onClick(e, columnIndex);
|
||||
@@ -47,22 +41,20 @@ const HeaderCell = ({
|
||||
return;
|
||||
}
|
||||
if(contextMenuOptions && contextMenuOptions.length > 0){
|
||||
|
||||
contextMenuLocationRef.current.x = event.clientX;
|
||||
contextMenuLocationRef.current.y = event.clientY;
|
||||
setOpenContextMenu(true);
|
||||
}
|
||||
},
|
||||
[contextMenuOptions, columnIndex] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
);
|
||||
const handleMenuItemClick = (event, index) => {
|
||||
contextMenuOptions[index].click({event, columnIndex});
|
||||
const handleMenuItemClick = (event, clickOption) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
clickOption({event, columnIndex, rowIndex, data: data.items[rowIndex][columnIndex]?.props?.rowData || {}});
|
||||
setOpenContextMenu(false);
|
||||
};
|
||||
const handleClose = (event) => {
|
||||
if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
setOpenContextMenu(false);
|
||||
};
|
||||
|
||||
const handleClicks = useSingleAndDoubleClick(handleClick, handleDoubleClick);
|
||||
|
||||
return (
|
||||
@@ -73,31 +65,11 @@ const HeaderCell = ({
|
||||
</Typography>
|
||||
{isFiltered && <FontAwesomeIcon icon={faFilter} />}
|
||||
{sortIndicatorIndex === columnIndex && (sortDirection === 'ASC' ? <div>↑</div> : <div>↓</div>)}
|
||||
<Popper open={openContextMenu} anchorEl={dropdownAnchorRef.current} role={undefined} transition style={{zIndex: 40}}>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
|
||||
}}
|
||||
>
|
||||
<Paper variant="outlined" style={{backgroundColor: theme.palette.mode === 'dark' ? theme.palette.primary.dark : theme.palette.primary.light}}>
|
||||
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
|
||||
<MenuList id="split-button-menu" >
|
||||
{contextMenuOptions.map((option, index) => (
|
||||
<MenuItem
|
||||
key={option.name + index}
|
||||
onClick={(event) => handleMenuItemClick(event, index)}
|
||||
>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
<ContextMenu dropdownAnchorRef={dropdownAnchorRef} contextMenuOptions={contextMenuOptions}
|
||||
disableFilterMenu={item?.disableFilterMenu} openContextMenu={openContextMenu}
|
||||
contextMenuLocationRef={contextMenuLocationRef}
|
||||
setOpenContextMenu={setOpenContextMenu} handleMenuItemClick={handleMenuItemClick}
|
||||
/>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
|
||||
+5
-2
@@ -11,7 +11,7 @@ import {GetMythicSetting, useSetMythicSetting} from "../MythicSavedUserSetting";
|
||||
|
||||
const HeaderCellContext = createContext({});
|
||||
|
||||
const MIN_COLUMN_WIDTH = 20;
|
||||
const MIN_COLUMN_WIDTH = 100;
|
||||
const MIN_FLEX_COLUMN_WIDTH = 150;
|
||||
|
||||
|
||||
@@ -195,7 +195,10 @@ const ResizableGridWrapper = ({
|
||||
return itemRow[columnIndex]?.props?.cellData.length;
|
||||
}
|
||||
if(columns[columnIndex].type === "size"){
|
||||
if(itemRow[columnIndex]?.props?.cellData){
|
||||
if(itemRow[columnIndex]?.props?.cellData !== undefined){
|
||||
if(typeof itemRow[columnIndex]?.props?.cellData === 'number'){
|
||||
return String(itemRow[columnIndex]?.props?.cellData)?.length;
|
||||
}
|
||||
return String(itemRow[columnIndex]?.props?.cellData?.plaintext)?.length;
|
||||
}
|
||||
return itemRow[columnIndex].length;
|
||||
|
||||
@@ -90,12 +90,14 @@ const openedMixin = (theme) => ({
|
||||
overflowX: 'hidden',
|
||||
borderRadius: "0 !important",
|
||||
border: "0px !important",
|
||||
//backgroundColor: "unset !important",
|
||||
});
|
||||
const closedMixin = (theme) => ({
|
||||
overflowX: 'hidden',
|
||||
width: `calc(${theme.spacing(4)} + 1px)`,
|
||||
borderRadius: "0 !important",
|
||||
border: "0px !important",
|
||||
border: "0px !important",
|
||||
//backgroundColor: "unset !important",
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
width: `calc(${theme.spacing(5)} + 1px)`,
|
||||
borderRadius: "0 !important",
|
||||
@@ -108,28 +110,38 @@ const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open'
|
||||
flexShrink: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
boxSizing: 'border-box',
|
||||
background: `linear-gradient(${theme.topAppBarColor}, ${theme.topAppBarBottomColor}) !important`,
|
||||
//backgroundColor: `${theme.topAppBarColor} !important`,
|
||||
[`& .${classes.listSubHeader}`]: {
|
||||
//color: ` ${theme.navBarTextIconColor} !important`,
|
||||
backgroundColor: `${theme.topAppBarColor} !important`,
|
||||
//backgroundColor: `${theme.topAppBarColor} !important`,
|
||||
backgroundColor: "unset !important",
|
||||
},
|
||||
[`& .${classes.listSubHeader}:hover`]: {
|
||||
backgroundColor: `${theme.topAppBarColor} !important`,
|
||||
//backgroundColor: `${theme.topAppBarColor} !important`,
|
||||
filter: `brightness(90%)`,
|
||||
color: `${theme.navBarTextIconColor} !important`,
|
||||
backgroundColor: "unset !important",
|
||||
},
|
||||
variants: [
|
||||
{
|
||||
props: ({ open }) => open,
|
||||
style: {
|
||||
...openedMixin(theme),
|
||||
'& .MuiDrawer-paper': openedMixin(theme),
|
||||
'& .MuiDrawer-paper': {
|
||||
...openedMixin(theme),
|
||||
backgroundColor: "unset !important",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
props: ({ open }) => !open,
|
||||
style: {
|
||||
...closedMixin(theme),
|
||||
'& .MuiDrawer-paper': closedMixin(theme),
|
||||
'& .MuiDrawer-paper': {
|
||||
...closedMixin(theme),
|
||||
backgroundColor: "unset !important",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -143,6 +155,7 @@ export const StyledListItem = styled(ListItem)(
|
||||
marginTop: 0,
|
||||
paddingBottom: "2px",
|
||||
color: theme.navBarTextColor,
|
||||
backgroundColor: "unset !important",
|
||||
}),
|
||||
);
|
||||
export const StyledListItemIcon = styled(ListItemIcon)(
|
||||
@@ -152,6 +165,7 @@ export const StyledListItemIcon = styled(ListItemIcon)(
|
||||
paddingBottom: 0,
|
||||
minWidth: "45px",
|
||||
//color: theme.navBarTextIconColor,
|
||||
backgroundColor: "unset !important",
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -736,9 +750,10 @@ export function TopAppBarVertical(props) {
|
||||
<>
|
||||
{me?.user?.current_operation_id ? (<EventFeedNotifications me={me} />) : null }
|
||||
<Drawer anchor="left" variant="permanent" open={menuOpen} onClose={handleDrawerClose}
|
||||
style={{borderRight: "1px solid grey !important"}}>
|
||||
style={{borderRight: "1px solid grey !important",
|
||||
}}>
|
||||
<List style={{paddingTop: 0, marginTop: 0, height: "100%", display: "flex", flexDirection: "column",
|
||||
backgroundColor: theme.topAppBarColor,
|
||||
backgroundColor: "unset !important",
|
||||
borderBottom: "unset !important", borderLeft: "unset !important", borderTop: "unset !important"}}>
|
||||
<ListItem className={classes.listSubHeader} style={{marginTop:0, paddingTop: 0, paddingLeft: "2px", paddingBottom: 0}}>
|
||||
<ListItemIcon >
|
||||
|
||||
@@ -146,8 +146,16 @@ export const taskingDataFragment = gql`
|
||||
command_name
|
||||
opsec_pre_blocked
|
||||
opsec_pre_bypassed
|
||||
opsec_pre_bypass_user {
|
||||
id
|
||||
username
|
||||
}
|
||||
opsec_post_blocked
|
||||
opsec_post_bypassed
|
||||
opsec_post_bypass_user {
|
||||
id
|
||||
username
|
||||
}
|
||||
is_interactive_task
|
||||
interactive_task_type
|
||||
has_intercepted_response
|
||||
|
||||
@@ -19,8 +19,12 @@ export const getCallbackIdFromClickedTab = (tabId) => {
|
||||
return Number(tabId.split("fileBrowser")[0]);
|
||||
}else if(tabId.includes("interact")){
|
||||
return Number(tabId.split("interact")[0]);
|
||||
}else if(tabId.includes("processBrowser")){
|
||||
}else if(tabId.includes("processBrowser")) {
|
||||
return Number(tabId.split("processBrowser")[0]);
|
||||
}else if(tabId.includes("customFileBasedBrowser")) {
|
||||
return Number(tabId.split("customFileBasedBrowser")[0]);
|
||||
}else if(tabId.includes("customProcessBasedBrowser")) {
|
||||
return Number(tabId.split("customProcessBasedBrowser")[0]);
|
||||
} else {
|
||||
console.log("unknown tab type", tabId);
|
||||
return 0;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useMemo, useContext} from 'react';
|
||||
import {MythicDialog} from '../../MythicComponents/MythicDialog';
|
||||
import {MythicDialog, MythicModifyStringDialog} from '../../MythicComponents/MythicDialog';
|
||||
import {
|
||||
exportCallbackConfigQuery,
|
||||
hideCallbackMutation, lockCallbackMutation, unlockCallbackMutation, updateCallbackTriggerMutation,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
updateSleepInfoCallbackMutation
|
||||
} from './CallbackMutations';
|
||||
import {snackActions} from '../../utilities/Snackbar';
|
||||
import {useMutation, useLazyQuery } from '@apollo/client';
|
||||
import {useMutation, useLazyQuery, gql } from '@apollo/client';
|
||||
import {
|
||||
CallbacksTableIDCell,
|
||||
CallbacksTableStringCell,
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
CallbacksTableIPCell, CallbacksTableTagsCell
|
||||
} from './CallbacksTableRow';
|
||||
import MythicResizableGrid from '../../MythicComponents/MythicResizableGrid';
|
||||
import {TableFilterDialog} from './TableFilterDialog';
|
||||
import {CallbacksTabsHideMultipleDialog, CallbacksTabsSelectMultipleDialog} from "./CallbacksTabsHideMultipleDialog";
|
||||
import {CallbacksTabsTaskMultipleDialog} from "./CallbacksTabsTaskMultipleDialog";
|
||||
import ip6 from 'ip6';
|
||||
@@ -56,7 +55,26 @@ import {CallbacksTableColumnsReorderDialog} from "./CallbacksTableColumnsReorder
|
||||
import PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwoTone';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import {EventTriggerContextSelectDialog} from "../Eventing/EventTriggerContextSelect";
|
||||
import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery";
|
||||
import {faFilter} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export const getCustomBrowsersQuery = gql(`
|
||||
query getCustomBrowsersQuery{
|
||||
custombrowser(where: {deleted: {_eq: false}}, order_by: {name: asc}){
|
||||
id
|
||||
name
|
||||
type
|
||||
separator
|
||||
columns
|
||||
default_visible_columns
|
||||
indicate_partial_listing
|
||||
show_current_path
|
||||
extra_table_inputs
|
||||
row_actions
|
||||
export_function
|
||||
}
|
||||
}
|
||||
`);
|
||||
export const ipCompare = (a, b) => {
|
||||
let aJSON = JSON.parse(a);
|
||||
if(aJSON.length === 0){return 0}
|
||||
@@ -316,7 +334,8 @@ function CallbacksTablePreMemo(props){
|
||||
console.log(data);
|
||||
snackActions.error("Failed to export configuration: " + data.message)
|
||||
}
|
||||
})
|
||||
});
|
||||
const getCustomBrowsers = useMythicLazyQuery(getCustomBrowsersQuery, {fetchPolicy: "no-cache"});
|
||||
const taskingData = React.useRef({"parameters": "", "ui_feature": "callback_table:exit"});
|
||||
const [openTaskingButton, setOpenTaskingButton] = React.useState(false);
|
||||
const eventingDataRef = React.useRef({});
|
||||
@@ -327,7 +346,10 @@ function CallbacksTablePreMemo(props){
|
||||
updateTrigger({variables: {callback_display_id: openTriggerDialog.display_id, trigger_on_checkin_after_time: newTriggerValue}})
|
||||
setOpenTriggerDialog({...openTriggerDialog, open: false});
|
||||
}
|
||||
const onRowContextClick = ({rowDataStatic}) => {
|
||||
async function getCustomBrowsersForContextMenu(){
|
||||
return await getCustomBrowsers({}).then(result => {return result.data?.custombrowser});
|
||||
}
|
||||
async function onRowContextClick({rowDataStatic}) {
|
||||
// based on row, return updated options array?
|
||||
let defaultInteractIcon = <KeyboardIcon style={{paddingRight: "5px"}}/>;
|
||||
if(interactType === "interactSplit"){
|
||||
@@ -335,6 +357,26 @@ function CallbacksTablePreMemo(props){
|
||||
} else if(interactType === "interactConsole"){
|
||||
defaultInteractIcon = <TerminalIcon style={{paddingRight: "5px"}}/>;
|
||||
}
|
||||
let customBrowsers = await getCustomBrowsersForContextMenu();
|
||||
customBrowsers = customBrowsers.map(b => {
|
||||
return {
|
||||
name: b.name,
|
||||
icon: b.type === 'file' ? <FontAwesomeIcon icon={faFolderOpen} style={{color: theme.folderColor, cursor: "pointer", marginRight: "10px"}} /> : <AccountTreeIcon style={{paddingRight: "5px"}}/>,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const tabType = b.type === 'file' ? 'customFileBasedBrowser' : 'customProcessBasedBrowser';
|
||||
onOpenTab({
|
||||
tabType: tabType,
|
||||
tabID: rowDataStatic.id + tabType + b.name,
|
||||
callbackID: rowDataStatic.id,
|
||||
color: rowDataStatic.color,
|
||||
displayID: rowDataStatic.display_id,
|
||||
customBrowser: b
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
return [
|
||||
{
|
||||
name: <div style={{display: "flex", flexDirection: "column"}}>
|
||||
@@ -433,6 +475,7 @@ function CallbacksTablePreMemo(props){
|
||||
{
|
||||
name: 'File Browser', icon: <FontAwesomeIcon icon={faFolderOpen} style={{color: theme.folderColor, cursor: "pointer", marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const tabType = "fileBrowser";
|
||||
onOpenTab({
|
||||
@@ -446,6 +489,7 @@ function CallbacksTablePreMemo(props){
|
||||
{
|
||||
name: 'Process Browser', icon: <AccountTreeIcon style={{paddingRight: "5px"}}/>,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const tabType = "processBrowser";
|
||||
onOpenTab({
|
||||
@@ -456,6 +500,17 @@ function CallbacksTablePreMemo(props){
|
||||
displayID: rowDataStatic.display_id});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: <div style={{display: "flex", flexDirection: "column", width: "100%"}}>
|
||||
<Typography>
|
||||
{"Custom Agent Browsers:"}
|
||||
</Typography>
|
||||
</div>,
|
||||
icon: null, click: ({event}) => {},
|
||||
type: "item",
|
||||
disabled: true
|
||||
},
|
||||
...customBrowsers
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -583,10 +638,10 @@ function CallbacksTablePreMemo(props){
|
||||
}
|
||||
];
|
||||
}
|
||||
const callbackDropdown = ({rowDataStatic, event}) => {
|
||||
callbackDropdownRef.current.options = onRowContextClick({rowDataStatic});
|
||||
async function callbackDropdown({rowDataStatic, event}) {
|
||||
callbackDropdownRef.current.options = await onRowContextClick({rowDataStatic});
|
||||
callbackDropdownRef.current.callback = rowDataStatic;
|
||||
callbackDropdownRef.current.dropdownAnchorRef = event.currentTarget;
|
||||
callbackDropdownRef.current.dropdownAnchorRef = event?.currentTarget || event.target;
|
||||
setOpenCallbackDropdown(true);
|
||||
}
|
||||
React.useEffect( () => {
|
||||
@@ -679,23 +734,28 @@ function CallbacksTablePreMemo(props){
|
||||
}, []);
|
||||
const contextMenuOptions = [
|
||||
{
|
||||
name: 'Filter Column',
|
||||
name: 'Filter Column', type: "item",
|
||||
icon: <FontAwesomeIcon icon={faFilter} style={{paddingRight: "5px"}} />,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
if(columns[columnIndex].disableFilterMenu){
|
||||
snackActions.warning("Can't filter that column");
|
||||
return;
|
||||
}
|
||||
setSelectedColumn(columns[columnIndex]);
|
||||
setOpenContextMenu(true);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Reorder Columns and Adjust Visibility",
|
||||
name: "Reorder Columns and Adjust Visibility", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
setOpenReorderDialog(true);
|
||||
}
|
||||
}
|
||||
@@ -882,10 +942,10 @@ function CallbacksTablePreMemo(props){
|
||||
}
|
||||
}, [])
|
||||
}, [callbacks, sortData, filterOptions, columnVisibility, clickedCallbackID]);
|
||||
const onSubmitFilterOptions = (newFilterOptions) => {
|
||||
setFilterOptions(newFilterOptions);
|
||||
const onSubmitFilterOptions = (value) => {
|
||||
setFilterOptions({...filterOptions, [selectedColumn.key]: value });
|
||||
try{
|
||||
updateSetting({setting_name: "callbacks_table_filter_options", value: newFilterOptions});
|
||||
updateSetting({setting_name: "callbacks_table_filter_options", value: {...filterOptions, [selectedColumn.key]: value }});
|
||||
}catch(error){
|
||||
console.log("failed to save filter options");
|
||||
}
|
||||
@@ -963,11 +1023,14 @@ function CallbacksTablePreMemo(props){
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={<TableFilterDialog
|
||||
selectedColumn={selectedColumn}
|
||||
filterOptions={filterOptions}
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
onClose={()=>{setOpenContextMenu(false);}} />}
|
||||
innerDialog={<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={filterOptions[selectedColumn.key]}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>}
|
||||
/>
|
||||
}
|
||||
{openEditDescriptionDialog &&
|
||||
|
||||
@@ -10,6 +10,10 @@ import { CallbacksTabsFileBrowserLabel, CallbacksTabsFileBrowserPanel } from './
|
||||
import { CallbacksTabsProcessBrowserLabel, CallbacksTabsProcessBrowserPanel } from './CallbacksTabsProcessBrowser';
|
||||
import { CallbacksTabsTaskingSplitLabel, CallbacksTabsTaskingSplitPanel} from "./CallbacksTabsTaskingSplit";
|
||||
import {CallbacksTabsTaskingConsoleLabel, CallbacksTabsTaskingConsolePanel} from "./CallbacksTabsTaskingConsole";
|
||||
import {
|
||||
CallbacksTabsCustomFileBasedBrowserLabel,
|
||||
CallbacksTabsCustomFileBasedBrowserPanel
|
||||
} from "./CallbacksTabsCustomFileBasedBrowser";
|
||||
|
||||
export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clickedTabId, setClickedTabId, onEditTabDescription, contextMenuOptions, me, newDataForTab, setNewDataForTab}) {
|
||||
|
||||
@@ -134,12 +138,12 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clic
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
/>
|
||||
)
|
||||
case 'fileBrowser':
|
||||
case 'customFileBasedBrowser':
|
||||
return (
|
||||
<CallbacksTabsFileBrowserLabel
|
||||
<CallbacksTabsCustomFileBasedBrowserLabel
|
||||
onEditTabDescription={onEditTabDescription}
|
||||
onCloseTab={onCloseTabLocal}
|
||||
key={'tablabel' + tab.tabID + tab.tabType}
|
||||
key={'tablabel' + tab.tabID + tab.tabType + tab?.customBrowser?.name}
|
||||
tabInfo={tab}
|
||||
index={index}
|
||||
me={me}
|
||||
@@ -164,6 +168,21 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clic
|
||||
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;
|
||||
}
|
||||
@@ -237,6 +256,25 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clic
|
||||
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
|
||||
@@ -254,6 +292,7 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clic
|
||||
index={index}
|
||||
me={me}
|
||||
parentMountedRef={mountedRef}
|
||||
baseUIFeature={"file_browser"}
|
||||
/>
|
||||
);
|
||||
case 'processBrowser':
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+976
@@ -0,0 +1,976 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useMutation, useLazyQuery, gql } from '@apollo/client';
|
||||
import { snackActions } from '../../utilities/Snackbar';
|
||||
import {
|
||||
MythicDialog,
|
||||
MythicModifyStringDialog,
|
||||
MythicViewJSONAsTableDialog,
|
||||
} from '../../MythicComponents/MythicDialog';
|
||||
import {faFolder} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import DescriptionIcon from '@mui/icons-material/Description';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import ErrorIcon from '@mui/icons-material/Error';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { IconButton } from '@mui/material';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import ListIcon from '@mui/icons-material/List';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import GetAppIcon from '@mui/icons-material/GetApp';
|
||||
import 'react-virtualized/styles.css';
|
||||
import FileCopyOutlinedIcon from '@mui/icons-material/FileCopyOutlined';
|
||||
import { copyStringToClipboard } from '../../utilities/Clipboard';
|
||||
import MythicResizableGrid from '../../MythicComponents/MythicResizableGrid';
|
||||
import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip';
|
||||
import {faFilter} from '@fortawesome/free-solid-svg-icons';
|
||||
import {MythicTransferListDialog} from '../../MythicComponents/MythicTransferList';
|
||||
import {TagsDisplay, ViewEditTags} from '../../MythicComponents/MythicTag';
|
||||
import { toLocalTime } from '../../utilities/Time';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import {RenderSingleTask} from "../SingleTaskView/SingleTaskView";
|
||||
import {
|
||||
GetComputedFontSize,
|
||||
GetMythicSetting,
|
||||
useSetMythicSetting
|
||||
} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {getIconColor, getIconName} from "./ResponseDisplayTable";
|
||||
import {CallbacksTableColumnsReorderDialog} from "./CallbacksTableColumnsReorderDialog";
|
||||
|
||||
const updateFileComment = gql`
|
||||
mutation updateCommentMutation($mythictree_id: Int!, $comment: String!) {
|
||||
update_mythictree_by_pk(pk_columns: { id: $mythictree_id }, _set: { comment: $comment }) {
|
||||
comment
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CallbacksTabsCustomFileBasedBrowserTable = (props) => {
|
||||
const theme = useTheme();
|
||||
const [updateSetting] = useSetMythicSetting();
|
||||
const [allData, setAllData] = React.useState([]);
|
||||
const [openReorderDialog, setOpenReorderDialog] = React.useState(false);
|
||||
const [openContextMenu, setOpenContextMenu] = React.useState(false);
|
||||
const [filterOptions, setFilterOptions] = React.useState({});
|
||||
const [selectedColumn, setSelectedColumn] = React.useState({});
|
||||
const [fileCommentDialogOpen, setFileCommentDialogOpen] = React.useState(false);
|
||||
const [viewPermissionsDialogOpen, setViewPermissionsDialogOpen] = React.useState(false);
|
||||
const permissionDataRef = React.useRef({metadata: {}, name: ""});
|
||||
const commentDataRef = React.useRef({id: 0, comment: ""});
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [sortData, setSortData] = React.useState({"sortKey": null, "sortDirection": null, "sortType": null})
|
||||
const [columnVisibility, setColumnVisibility] = React.useState({
|
||||
"visible": [...props.treeConfig.table.visible],
|
||||
"hidden": [...props.treeConfig.table.hidden]
|
||||
});
|
||||
const indicatePartialListingRef = React.useRef(props.treeConfig.indicate_partial_listing);
|
||||
const [selectedRows, setSelectedRows] = React.useState([]);
|
||||
const [updateComment] = useMutation(updateFileComment, {
|
||||
onCompleted: (data) => {
|
||||
snackActions.success('updated comment');
|
||||
},
|
||||
});
|
||||
const onSubmitUpdatedComment = (comment) => {
|
||||
updateComment({ variables: { mythictree_id: commentDataRef.current.id, comment: comment } });
|
||||
};
|
||||
const [initialDefaultColumns, setInitialDefaultColumns] = React.useState([
|
||||
{ name: 'name', type: 'name', key: 'name_text', fillWidth: true, visible: true },
|
||||
...props.treeConfig.table.columns,
|
||||
{ name: 'tags', type: 'tags', key: 'tags', disableSort: true, disableFilterMenu: true, width: 220 },
|
||||
{ name: 'comment', type: 'comment', key: 'comment', width: 200 },
|
||||
]);
|
||||
const [columnOrder, setColumnOrder] = React.useState(initialDefaultColumns);
|
||||
const columns = React.useMemo(
|
||||
() =>
|
||||
columnOrder.reduce( (prev, cur) => {
|
||||
if(columnVisibility.visible.includes(cur.name)){
|
||||
if(filterOptions[cur.key] && String(filterOptions[cur.key]).length > 0){
|
||||
return [...prev, {...cur, filtered: true}];
|
||||
}else{
|
||||
return [...prev, {...cur}];
|
||||
}
|
||||
}else{
|
||||
return [...prev];
|
||||
}
|
||||
}, [])
|
||||
, [filterOptions, columnVisibility, columnOrder]
|
||||
);
|
||||
|
||||
const sortedData = React.useMemo(() => {
|
||||
if (sortData.sortKey === null || sortData.sortType === null) {
|
||||
return allData;
|
||||
}
|
||||
const tempData = [...allData];
|
||||
if (sortData.sortType === 'number' || sortData.sortType === 'size' || sortData.sortType === 'date') {
|
||||
tempData.sort((a, b) => {
|
||||
if(sortData.inMetadata){
|
||||
try {
|
||||
if(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a]?.metadata[sortData.sortKey] ===
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b]?.metadata[sortData.sortKey]){
|
||||
return 0;
|
||||
}
|
||||
if (props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a]?.metadata[sortData.sortKey] === null ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a]?.metadata[sortData.sortKey] === undefined) {
|
||||
return -1;
|
||||
}
|
||||
if (props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b]?.metadata[sortData.sortKey] === null ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b]?.metadata[sortData.sortKey] === undefined) {
|
||||
return 1;
|
||||
}
|
||||
return parseInt(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a]?.metadata[sortData.sortKey]) >
|
||||
parseInt(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b]?.metadata[sortData.sortKey]) ? 1 : -1;
|
||||
}catch(error) {
|
||||
console.log("failed to parse data for sorting", error);
|
||||
return props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a]?.metadata[sortData.sortKey] >
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b]?.metadata[sortData.sortKey] ? 1 : -1
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
if(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] ===
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey]){
|
||||
return 0;
|
||||
}
|
||||
if (props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] === null ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] === undefined) {
|
||||
return -1;
|
||||
}
|
||||
if (props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey] === null ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey] === undefined) {
|
||||
return 1;
|
||||
}
|
||||
return parseInt(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey]) > parseInt(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey]) ? 1 : -1;
|
||||
} catch (error) {
|
||||
console.log("failed to parse data for sorting", error);
|
||||
return props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] > props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
} else if (sortData.sortType === 'string') {
|
||||
tempData.sort((a, b) => {
|
||||
try{
|
||||
if(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] ===
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey]){
|
||||
return 0;
|
||||
}
|
||||
if (props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] === null ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] === undefined) {
|
||||
return -1;
|
||||
}
|
||||
if (props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey] === null ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey] === undefined) {
|
||||
return 1;
|
||||
}
|
||||
return props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey].localeCompare(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey]);
|
||||
}catch(error){
|
||||
console.log("failed to parse data for sorting", error);
|
||||
return props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] > props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey] ? 1 : -1
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
if (sortData.sortDirection === 'DESC') {
|
||||
tempData.reverse();
|
||||
}
|
||||
return tempData;
|
||||
}, [allData, sortData]);
|
||||
const onSubmitFilterOptions = (value) => {
|
||||
setFilterOptions({...filterOptions, [selectedColumn.key]: value });
|
||||
try{
|
||||
updateSetting({setting_name: `${props.treeConfig.name}_browser_filter_options`, value: {...filterOptions, [selectedColumn.key]: value }});
|
||||
}catch(error){
|
||||
console.log("failed to save filter options");
|
||||
}
|
||||
}
|
||||
const filterRow = (row) => {
|
||||
if(props.treeRootData[props.selectedFolderData.group] === undefined ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host] === undefined ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row] === undefined) {
|
||||
return true;
|
||||
}
|
||||
if(!props.showDeletedFiles && props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.deleted){
|
||||
return true;
|
||||
}
|
||||
for(const [key,value] of Object.entries(filterOptions)){
|
||||
if(["name_text", "comment"].includes(key)){
|
||||
if(!String(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row][key]).toLowerCase().includes(value)){
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if(!String(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row].metadata[key]).toLowerCase().includes(value)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const gridData = React.useMemo(
|
||||
() =>
|
||||
// row is just the name
|
||||
sortedData.reduce((prev, row) => {
|
||||
if(filterRow(row)){
|
||||
return [...prev];
|
||||
}else{
|
||||
return [...prev, columns.map( c => {
|
||||
switch(c.type) {
|
||||
case "name":
|
||||
return <FileBrowserTableRowNameCell
|
||||
treeRootData={props.treeRootData[props.selectedFolderData.group]}
|
||||
selectedFolderData={props.selectedFolderData}
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row].name_text}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}/>;
|
||||
case "tags":
|
||||
return <FileBrowserTagsCell
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}
|
||||
treeRootData={props.treeRootData[props.selectedFolderData.group]}
|
||||
cellData={row}
|
||||
selectedFolderData={props.selectedFolderData}
|
||||
me={props.me}/>
|
||||
case "comment":
|
||||
return <FileBrowserTableRowStringCell
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row].comment}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}/>
|
||||
case "date":
|
||||
return <TableRowDateCell
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.metadata?.[c.key] || null}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}
|
||||
view_utc_time={props.me?.user?.view_utc_time}/>
|
||||
case "size":
|
||||
return <TableRowSizeCell
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.metadata?.[c.key] || 0}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}/>
|
||||
case "string":
|
||||
return <FileBrowserTableRowStringCell
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.metadata?.[c.key] || ""}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}/>
|
||||
default:
|
||||
return <FileBrowserTableRowStringCell
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.metadata?.[c.key] || ""}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}/>
|
||||
}
|
||||
})];
|
||||
}
|
||||
|
||||
}, []),
|
||||
[sortedData, props.onTaskRowAction, filterOptions, columnVisibility, props.showDeletedFiles]
|
||||
);
|
||||
const getDisplayFormat = () => {
|
||||
if(sortedData.length === 0 && props?.selectedFolderData?.success === false){
|
||||
return "showTask";
|
||||
}else if(sortedData.length > 0 || props?.selectedFolderData?.success){
|
||||
return "normal";
|
||||
} else if(sortedData.length === 0 && props?.selectedFolderData?.has_children && props?.selectedFolderData?.success === null){
|
||||
return "fetchLocal";
|
||||
}else if(sortedData.length === 0 && !props?.selectedFolderData?.has_children && props?.selectedFolderData?.success === null) {
|
||||
return "fetchRemote";
|
||||
}else if(sortedData.length === 0){
|
||||
return "fetchRemote";
|
||||
} else {
|
||||
return "normal";
|
||||
}
|
||||
}
|
||||
const displayFormat = getDisplayFormat();
|
||||
useEffect(() => {
|
||||
// when the folder changes, we need to aggregate all of the entries
|
||||
//console.log(props.selectedFolderData, props.treeAdjMatrix, props.treeRootData)
|
||||
let desiredPath = props.selectedFolderData.full_path_text;
|
||||
if(props.selectedFolderData.id === props.selectedFolderData.host){
|
||||
desiredPath = "";
|
||||
}
|
||||
let newAllData = Object.keys(props.treeAdjMatrix[props.selectedFolderData.group]?.[props.selectedFolderData.host]?.[desiredPath] || {});
|
||||
setAllData(newAllData);
|
||||
//console.log("just set all data")
|
||||
}, [props.selectedFolderData, props.treeAdjMatrix]);
|
||||
const onRowDoubleClick = (e, rowIndex, rowData) => {
|
||||
if (!rowData.can_have_children) {
|
||||
return;
|
||||
}
|
||||
snackActions.info('Fetching contents from database...');
|
||||
props.onRowDoubleClick({...rowData, group: props.selectedFolderData.group});
|
||||
setSortData({"sortKey": null, "sortType":null, "sortDirection": "ASC"});
|
||||
};
|
||||
const onClickHeader = (e, columnIndex) => {
|
||||
const column = columns[columnIndex];
|
||||
if(column.disableSort){
|
||||
return;
|
||||
}
|
||||
if (!column.key) {
|
||||
setSortData({"sortKey": null, "sortType":null, "sortDirection": "ASC", "inMetadata": false});
|
||||
}
|
||||
if (sortData.sortKey === column.key) {
|
||||
if (sortData.sortDirection === 'ASC') {
|
||||
setSortData({...sortData, "sortDirection": "DESC"});
|
||||
} else {
|
||||
setSortData({"sortKey": null, "sortType":null, "sortDirection": "ASC", "inMetadata": false});
|
||||
}
|
||||
} else {
|
||||
setSortData({"sortKey": column.key, "inMetadata": column.inMetadata, "sortType":column.type, "sortDirection": "ASC"});
|
||||
}
|
||||
};
|
||||
const contextMenuOptions = [
|
||||
{
|
||||
name: 'Filter Column', type: "item",
|
||||
icon: <FontAwesomeIcon icon={faFilter} style={{paddingRight: "5px"}} />,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
if(columns[columnIndex].disableFilterMenu){
|
||||
snackActions.warning("Can't filter that column");
|
||||
return;
|
||||
}
|
||||
setSelectedColumn(columns[columnIndex]);
|
||||
setOpenContextMenu(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Reorder Columns and Adjust Visibility", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
setOpenReorderDialog(true);
|
||||
}
|
||||
}
|
||||
];
|
||||
const sortColumn = columns.findIndex((column) => column.key === sortData.sortKey);
|
||||
const onRowClick = ({event, rowDataStatic}) => {
|
||||
if(event.ctrlKey || event.metaKey){
|
||||
if(props.treeRootData?.[props.selectedFolderData.group]?.[rowDataStatic.host]?.[rowDataStatic.full_path_text]){
|
||||
props.treeRootData[props.selectedFolderData.group][rowDataStatic.host][rowDataStatic.full_path_text].selected = true;
|
||||
}
|
||||
|
||||
setSelectedRows([...selectedRows, rowDataStatic]);
|
||||
} else {
|
||||
if(props.treeRootData?.[props.selectedFolderData.group]?.[rowDataStatic.host]?.[rowDataStatic.full_path_text]){
|
||||
props.treeRootData[props.selectedFolderData.group][rowDataStatic.host][rowDataStatic.full_path_text].selected = true;
|
||||
}
|
||||
for(let i = 0; i < selectedRows.length; i++){
|
||||
if(props.treeRootData?.[props.selectedFolderData.group]?.[selectedRows[i].host]?.[selectedRows[i].full_path_text]){
|
||||
props.treeRootData[props.selectedFolderData.group][selectedRows[i].host][selectedRows[i].full_path_text].selected = false;
|
||||
}
|
||||
}
|
||||
setSelectedRows([rowDataStatic]);
|
||||
}
|
||||
}
|
||||
const optionsA = (element) => [
|
||||
{
|
||||
name: element?.name_text, icon: null, click: ({event}) => {},
|
||||
type: "item", disabled: true
|
||||
},
|
||||
{
|
||||
name: "Copy to Clipboard", icon: null, click: () => {}, type: "menu",
|
||||
menuItems: [
|
||||
{
|
||||
name: 'Name', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(element.name_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Full Path', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(element.full_path_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Metadata', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(JSON.stringify(element?.metadata, null, 2))){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'View Metadata', type: "item",
|
||||
icon: <VisibilityIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
permissionDataRef.current.metadata = element?.metadata;
|
||||
permissionDataRef.current.name = element.name_text;
|
||||
setViewPermissionsDialogOpen(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Edit Comment', type: "item",
|
||||
icon: <EditIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
commentDataRef.current.id = element.id;
|
||||
commentDataRef.current.comment = element.comment;
|
||||
setFileCommentDialogOpen(true);
|
||||
},
|
||||
},
|
||||
]
|
||||
async function optionsB (callback_id, callback_display_id, element) {
|
||||
let downloadCommand = await props.getLoadedCommandForUIFeature(callback_id, element.can_have_children ? props.baseUIFeature + ":download_folder" : props.baseUIFeature + ":download");
|
||||
let downloadDisplay = "Download (Unsupported)";
|
||||
if(element.can_have_children){
|
||||
downloadDisplay = "Download Folder (Unsupported)";
|
||||
}
|
||||
if(downloadCommand !== undefined){
|
||||
downloadDisplay = `Download (${downloadCommand.command.cmd})`;
|
||||
if(element.can_have_children){
|
||||
downloadDisplay = `Download Folder (${downloadCommand.command.cmd})`;
|
||||
}
|
||||
}
|
||||
let listCommand = await props.getLoadedCommandForUIFeature(callback_id, props.baseUIFeature + ":list");
|
||||
let listDisplay = "List (Unsupported)";
|
||||
if(listCommand !== undefined){
|
||||
listDisplay = `List (${listCommand.command.cmd})`;
|
||||
}
|
||||
let removeCommand = await props.getLoadedCommandForUIFeature(callback_id, props.baseUIFeature + ":remove");
|
||||
let removeDisplay = "Remove (Unsupported)";
|
||||
if(removeCommand !== undefined){
|
||||
removeDisplay = `Remove (${removeCommand.command.cmd})`;
|
||||
}
|
||||
return [
|
||||
{
|
||||
name: downloadDisplay, type: "item",
|
||||
disabled: downloadCommand === undefined,
|
||||
icon: <GetAppIcon color="success" style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
props.onTaskRowAction({
|
||||
path: element.parent_path_text,
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: element.can_have_children ? props.baseUIFeature + ':download_folder' : props.baseUIFeature + ':download',
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: listDisplay, type: "item",
|
||||
disabled: listCommand === undefined,
|
||||
icon: <ListIcon color="warning" style={{ paddingRight: '5px'}} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
props.onTaskRowAction({
|
||||
path: element.parent_path_text,
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: props.baseUIFeature + ':list',
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: removeDisplay, type: "item",
|
||||
disabled: removeCommand === undefined,
|
||||
icon: <DeleteIcon color="error" style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
props.onTaskRowAction({
|
||||
path: element.parent_path_text,
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: props.baseUIFeature + ':remove',
|
||||
getConfirmation: true,
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
async function optionsC (callback_id, callback_display_id, element, action_name, ui_feature, icon, color, openDialog, getConfirmation) {
|
||||
let command = await props.getLoadedCommandForUIFeature(callback_id, ui_feature);
|
||||
let commandDisplay = `${action_name} (Unsupported)`;
|
||||
if(command !== undefined){
|
||||
commandDisplay = `${action_name} (${command.command.cmd})`;
|
||||
}
|
||||
return {
|
||||
name: commandDisplay, type: "item",
|
||||
disabled: command === undefined,
|
||||
icon: <FontAwesomeIcon icon={getIconName(icon)} style={{marginRight: "5px", color: getIconColor(theme, color || "")}}/>,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
props.onTaskRowAction({
|
||||
path: element.parent_path_text,
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: ui_feature, openDialog, getConfirmation,
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
async function onRowContextClick({rowDataStatic}) {
|
||||
// based on row, return updated options array?
|
||||
if(selectedRows.length > 1){
|
||||
let downloadCommand = await props.getLoadedCommandForUIFeature(rowDataStatic.callback.id, rowDataStatic.can_have_children ? props.baseUIFeature + ":download_folder" : props.baseUIFeature + ":download");
|
||||
let listCommand = await props.getLoadedCommandForUIFeature(rowDataStatic.callback.id, props.baseUIFeature + ":list");
|
||||
let removeCommand = await props.getLoadedCommandForUIFeature(rowDataStatic.callback.id, props.baseUIFeature + ":remove");
|
||||
return [
|
||||
{
|
||||
name: `Download All Selected`, type: "item",
|
||||
disabled: downloadCommand === undefined,
|
||||
icon: <GetAppIcon color="success" style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
let newTasks = [];
|
||||
for(let i = 0; i < selectedRows.length; i++){
|
||||
newTasks.push({
|
||||
path: selectedRows[i].parent_path_text,
|
||||
full_path: selectedRows[i].full_path_text,
|
||||
host: selectedRows[i].host,
|
||||
filename: selectedRows[i].name_text,
|
||||
uifeature: selectedRows[i].can_have_children ? props.baseUIFeature + ':download_folder': props.baseUIFeature + ':download',
|
||||
callback_id: props.tabInfo.callbackID,
|
||||
callback_display_id: props.tabInfo.displayID
|
||||
});
|
||||
}
|
||||
props.onTaskRowActions(newTasks);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `Remove All Selected`, type: "item",
|
||||
disabled: removeCommand === undefined,
|
||||
icon: <DeleteIcon color="error" style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
let newTasks = [];
|
||||
for(let i = 0; i < selectedRows.length; i++){
|
||||
newTasks.push({
|
||||
path: selectedRows[i].parent_path_text,
|
||||
full_path: selectedRows[i].full_path_text,
|
||||
host: selectedRows[i].host,
|
||||
filename: selectedRows[i].name_text,
|
||||
uifeature: props.baseUIFeature + ':remove',
|
||||
getConfirmation: true,
|
||||
callback_id: props.tabInfo.callbackID,
|
||||
callback_display_id: props.tabInfo.displayID
|
||||
});
|
||||
}
|
||||
props.onTaskRowActions(newTasks);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `List All Selected`, type: "item",
|
||||
disabled: listCommand === undefined,
|
||||
icon: <ListIcon color="warning" style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
let newTasks = [];
|
||||
for(let i = 0; i < selectedRows.length; i++){
|
||||
newTasks.push({
|
||||
path: selectedRows[i].parent_path_text,
|
||||
full_path: selectedRows[i].full_path_text,
|
||||
host: selectedRows[i].host,
|
||||
filename: selectedRows[i].name_text,
|
||||
uifeature: props.baseUIFeature + ':list',
|
||||
callback_id: props.tabInfo.callbackID,
|
||||
callback_display_id: props.tabInfo.displayID
|
||||
});
|
||||
}
|
||||
props.onTaskRowActions(newTasks);
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
let options = [...optionsA(rowDataStatic)];
|
||||
options.push(...(await optionsB(props.tabInfo.callbackID, props.tabInfo.displayID, rowDataStatic)));
|
||||
if(props.treeConfig.row_actions.length > 0){
|
||||
let extraOptions = [];
|
||||
for(let i = 0; i < props.treeConfig.row_actions.length; i++){
|
||||
let r = props.treeConfig.row_actions[i];
|
||||
if((r.supports_file && !rowDataStatic.can_have_children) || (r.supports_folder && rowDataStatic.can_have_children) ){
|
||||
extraOptions.push(await optionsC(rowDataStatic.callback.id, rowDataStatic.callback.display_id,
|
||||
rowDataStatic, r.name, r.ui_feature, r.icon, r.color, r.openDialog, r.getConfirmation));
|
||||
}
|
||||
}
|
||||
if(extraOptions.length > 0){
|
||||
options.push({
|
||||
name: `Custom Actions`, icon: null, click: () => {}, type: "menu",
|
||||
menuItems: extraOptions
|
||||
})
|
||||
}
|
||||
}
|
||||
if(rowDataStatic.callback.display_id !== props.tabInfo.displayID){
|
||||
options.push({
|
||||
name: `Original Callback: ${rowDataStatic.callback.display_id}`, icon: null, click: () => {}, type: "menu",
|
||||
menuItems: [
|
||||
...(await optionsB(rowDataStatic.callback.id, rowDataStatic.callback.display_id, rowDataStatic))
|
||||
]
|
||||
})
|
||||
}
|
||||
return options;
|
||||
}
|
||||
React.useEffect( () => {
|
||||
// on startup, want to see if `callbacks_table_columns` exists in storage and load it if possible
|
||||
try {
|
||||
const storageItem = GetMythicSetting({setting_name: `${props.treeConfig.name}_browser_table_columns`, default_value: [...props.treeConfig.table.visible]});
|
||||
if(storageItem !== null){
|
||||
let allColumns = [...columnVisibility["visible"].map(c => c), ...columnVisibility["hidden"].map(c => c)];
|
||||
let newHidden = [];
|
||||
allColumns.forEach((v,i,a) => {
|
||||
if(!storageItem.includes(v)){
|
||||
newHidden.push(v);
|
||||
}
|
||||
});
|
||||
if(storageItem.length !== 0){
|
||||
setColumnVisibility({visible: storageItem, hidden: newHidden});
|
||||
}
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_columns", error);
|
||||
}
|
||||
try {
|
||||
const storageItemOptions = GetMythicSetting({setting_name: `${props.treeConfig.name}_browser_filter_options`, default_value: {}});
|
||||
if(storageItemOptions !== null){
|
||||
setFilterOptions(storageItemOptions);
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_filter_options", error);
|
||||
}
|
||||
try {
|
||||
const storageColumnOrder = GetMythicSetting({setting_name: `${props.treeConfig.name}_browser_column_order`, default_value: columns.map(c => c.name)});
|
||||
if(storageColumnOrder !== null){
|
||||
let newOrder = [];
|
||||
for(let i = 0; i < storageColumnOrder.length; i++){
|
||||
for(let j = 0; j < columnOrder.length; j++){
|
||||
if(columnOrder[j].name === storageColumnOrder[i]){
|
||||
newOrder.push(columnOrder[j]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(newOrder.length === columns.length){
|
||||
setColumnOrder(newOrder);
|
||||
}
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_filter_options", error);
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
const onSubmitColumnReorder = (newOrder) => {
|
||||
let newVisible = [];
|
||||
let newHidden = [];
|
||||
for(let i = 0; i < newOrder.length; i++){
|
||||
if(newOrder[i].visible){
|
||||
newVisible.push(newOrder[i].name);
|
||||
} else {
|
||||
newHidden.push(newOrder[i].name);
|
||||
}
|
||||
}
|
||||
if(newVisible.length === 0){
|
||||
snackActions.error("Can't update to show no fields");
|
||||
return;
|
||||
}
|
||||
updateSetting({setting_name: `${props.treeConfig.name}_browser_column_order`, value: newOrder.map(c => c.name)});
|
||||
setColumnOrder(newOrder);
|
||||
setColumnVisibility({visible: newVisible, hidden: newHidden});
|
||||
updateSetting({setting_name: `${props.treeConfig.name}_browser_table_columns`, value: newVisible});
|
||||
setOpenReorderDialog(false);
|
||||
}
|
||||
const onResetColumnReorder = () => {
|
||||
onSubmitColumnReorder(initialDefaultColumns);
|
||||
}
|
||||
if(loading){
|
||||
return (
|
||||
<div style={{width: '100%', height: '100%', position: "relative",}}>
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%"
|
||||
}}>
|
||||
{"Loading Saved Browser Customizations..."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div style={{width: '100%', height: '100%', overflow: "hidden", position: "relative"}}>
|
||||
{displayFormat === "normal" &&
|
||||
<>
|
||||
<MythicResizableGrid
|
||||
columns={columns}
|
||||
sortIndicatorIndex={sortColumn}
|
||||
sortDirection={sortData.sortDirection}
|
||||
items={gridData}
|
||||
rowHeight={GetComputedFontSize() + 7}
|
||||
onClickHeader={onClickHeader}
|
||||
onDoubleClickRow={onRowDoubleClick}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
onRowContextMenuClick={onRowContextClick}
|
||||
onRowClick={onRowClick}
|
||||
/>
|
||||
{fileCommentDialogOpen && (
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
maxWidth='md'
|
||||
open={fileCommentDialogOpen}
|
||||
onClose={() => {
|
||||
setFileCommentDialogOpen(false);
|
||||
}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Edit Comment'
|
||||
onSubmit={onSubmitUpdatedComment}
|
||||
value={commentDataRef.current?.value}
|
||||
onClose={() => {
|
||||
setFileCommentDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{viewPermissionsDialogOpen && (
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
maxWidth='lg'
|
||||
open={viewPermissionsDialogOpen}
|
||||
onClose={() => {
|
||||
setViewPermissionsDialogOpen(false);
|
||||
}}
|
||||
innerDialog={
|
||||
<MythicViewJSONAsTableDialog
|
||||
title={'View Metadata for ' + permissionDataRef.current?.name}
|
||||
leftColumn='Attribute'
|
||||
rightColumn='Value'
|
||||
value={permissionDataRef.current?.metadata}
|
||||
onClose={() => {
|
||||
setViewPermissionsDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{openReorderDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openReorderDialog}
|
||||
onClose={()=>{setOpenReorderDialog(false);}}
|
||||
innerDialog={
|
||||
<CallbacksTableColumnsReorderDialog
|
||||
onClose={()=>{setOpenReorderDialog(false);}}
|
||||
visible={columnVisibility.visible}
|
||||
hidden={columnVisibility.hidden}
|
||||
onReset={onResetColumnReorder}
|
||||
onSubmit={onSubmitColumnReorder}
|
||||
initialItems={columnOrder}
|
||||
/>}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
}
|
||||
{displayFormat === "normal" && props?.selectedFolderData?.success === null && indicatePartialListingRef.current &&
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid black",
|
||||
padding: "5px",
|
||||
backgroundColor: "rgba(37,37,37,0.92)", color: "white",
|
||||
}}>
|
||||
{"Only PARTIAL data has been collected for this path. "}<br/>
|
||||
{"Task this callback to list the contents"}
|
||||
<IconButton style={{margin: 0, padding: 0, marginRight: "10px"}}
|
||||
onClick={props.onListFilesButtonFromTableWithNoEntries}>
|
||||
<RefreshIcon color={"info"} fontSize={"large"}
|
||||
style={{display: "inline-block",}}/>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{displayFormat === "fetchLocal" && indicatePartialListingRef.current &&
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid black",
|
||||
padding: "5px",
|
||||
backgroundColor: "rgba(37,37,37,0.92)", color: "white",
|
||||
}}>
|
||||
{"Some data exists for this path, but isn't loaded into the UI. "}
|
||||
<br/>
|
||||
{"Click the folder icon to fetch data from the database."}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{displayFormat === "fetchRemote" && indicatePartialListingRef.current &&
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid black",
|
||||
padding: "5px",
|
||||
backgroundColor: "rgba(37,37,37,0.92)", color: "white",
|
||||
}}>
|
||||
{"No data has been collected for this path. "}
|
||||
<div style={{display: "flex", alignItems: "center"}}>
|
||||
{"Task this callback to list the contents"}
|
||||
<IconButton style={{margin: 0, padding: 0, marginRight: "10px"}} onClick={props.onListFilesButtonFromTableWithNoEntries} >
|
||||
<RefreshIcon color={"info"} fontSize={"large"}
|
||||
style={{ display: "inline-block",}} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{displayFormat === "showTask" && props.selectedFolderData?.task_id > 0 &&
|
||||
<RenderSingleTask task_id={props.selectedFolderData?.task_id} />
|
||||
}
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openContextMenu}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={filterOptions[selectedColumn.key]}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const FileBrowserTableRowNameCell = ({cellData, rowData, treeRootData, selectedFolderData }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div style={{ alignItems: 'center', display: 'flex', maxHeight: "100%", textDecoration: treeRootData[selectedFolderData.host][rowData.full_path_text]?.deleted ? 'line-through' : '' }}>
|
||||
{!treeRootData[selectedFolderData.host][rowData.full_path_text]?.can_have_children ? (
|
||||
<DescriptionIcon size={"small"} style={{
|
||||
marginRight: '5px' }} />
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={faFolder}
|
||||
size={"1x"}
|
||||
style={{
|
||||
marginRight: '8px',
|
||||
marginLeft: "4px",
|
||||
color:
|
||||
treeRootData[selectedFolderData.host][rowData.full_path_text]?.success || treeRootData[selectedFolderData.host][rowData.full_path_text]?.has_children
|
||||
? theme.folderColor
|
||||
: theme.emptyFolderColor,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<pre
|
||||
style={{
|
||||
color:
|
||||
treeRootData[selectedFolderData.host][rowData.full_path_text]?.success !== null
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary,
|
||||
}}>
|
||||
{cellData}
|
||||
</pre>
|
||||
{treeRootData[selectedFolderData.host][rowData.full_path_text]?.success === true ? (
|
||||
<MythicStyledTooltip title='Successfully listed contents of folder' tooltipStyle={{display: "inline-flex", marginLeft: "5px"}}>
|
||||
<CheckCircleOutlineIcon color="success" fontSize='small' />
|
||||
</MythicStyledTooltip>
|
||||
) : treeRootData[selectedFolderData.host][rowData.full_path_text]?.success === false ? (
|
||||
<MythicStyledTooltip title='Failed to list contents of folder' tooltipStyle={{display: "inline-flex", marginLeft: "5px"}}>
|
||||
<ErrorIcon fontSize='small' color="error" />
|
||||
</MythicStyledTooltip>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const FileBrowserTagsCell = ({rowData, cellData, treeRootData, selectedFolderData, me}) => {
|
||||
return (
|
||||
<>
|
||||
<ViewEditTags target_object={"mythictree_id"} target_object_id={treeRootData[selectedFolderData.host][cellData]?.id} me={me} />
|
||||
<TagsDisplay tags={treeRootData[selectedFolderData.host][cellData]?.tags} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
const FileBrowserTableRowStringCell = ({ cellData, rowData }) => {
|
||||
//console.log(rowData, cellData)
|
||||
return (
|
||||
<>
|
||||
{cellData}
|
||||
</>
|
||||
)
|
||||
};
|
||||
export const TableRowDateCell = ({ cellData, rowData, view_utc_time }) => {
|
||||
|
||||
try{
|
||||
let cellDataInt = parseInt(cellData)
|
||||
if(cellData === "" || cellData === undefined || cellDataInt <= 0){
|
||||
return "";
|
||||
}
|
||||
let view_utc = true;
|
||||
if(view_utc_time !== undefined){
|
||||
view_utc = view_utc_time
|
||||
}
|
||||
// handle Unix epoch timestamps
|
||||
if (view_utc) {
|
||||
let init_date = new Date(cellDataInt);
|
||||
return init_date.toDateString() + " " + init_date.toTimeString().substring(0, 8) + " UTC";
|
||||
} else {
|
||||
let timezoneDate = new Date(cellDataInt);
|
||||
timezoneDate.setTime(timezoneDate.getTime() - (timezoneDate.getTimezoneOffset() *60*1000));
|
||||
return timezoneDate.toLocaleDateString() + " " + timezoneDate.toLocaleString([], {hour12: true, hour: "2-digit", minute: "2-digit"});
|
||||
}
|
||||
//const dateData = new Date(cellDataInt).toISOString();
|
||||
//return toLocalTime(dateData.slice(0, 10) + " " + dateData.slice(11,-1), view_utc_time);
|
||||
}catch(error){
|
||||
try{
|
||||
let cellDataInt = parseInt(cellData)
|
||||
// handle windows FILETIME values
|
||||
const dateData = new Date( ((cellDataInt / 10000000) - 11644473600) * 1000).toISOString();
|
||||
return toLocalTime(dateData.slice(0, 10) + " " + dateData.slice(11,-1), view_utc_time);
|
||||
}catch(error2){
|
||||
console.log("error with timestamp: ", cellData);
|
||||
return String(cellData);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
export const TableRowSizeCell = ({ cellData, rowData }) => {
|
||||
const getStringSize = () => {
|
||||
try {
|
||||
// process for getting human readable string from bytes: https://stackoverflow.com/a/18650828
|
||||
let bytes = parseInt(cellData);
|
||||
if (cellData === '' || cellData === undefined) return '';
|
||||
if (bytes === 0) return '0 B';
|
||||
const decimals = 2;
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
} catch (error) {
|
||||
return cellData;
|
||||
}
|
||||
};
|
||||
return getStringSize(cellData);
|
||||
};
|
||||
@@ -51,6 +51,7 @@ const fileDataFragment = gql`
|
||||
host
|
||||
id
|
||||
can_have_children
|
||||
has_children
|
||||
success
|
||||
full_path_text
|
||||
name_text
|
||||
@@ -68,7 +69,7 @@ const fileDataFragment = gql`
|
||||
const rootFileQuery = gql`
|
||||
${fileDataFragment}
|
||||
query myRootFolderQuery {
|
||||
mythictree(where: { parent_path_text: { _eq: "" }, tree_type: {_eq: "file"} }) {
|
||||
mythictree(where: { parent_path_text: { _eq: "" }, tree_type: {_eq: "file"} }, order_by: {id: desc}) {
|
||||
...fileObjData
|
||||
}
|
||||
}
|
||||
@@ -196,7 +197,7 @@ export function CallbacksTabsFileBrowserLabel(props) {
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNewDataForTab }) => {
|
||||
export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNewDataForTab, baseUIFeature }) => {
|
||||
const fromNow = React.useRef(getSkewedNow());
|
||||
const [backdropOpen, setBackdropOpen] = React.useState(false);
|
||||
const treeRootDataRef = React.useRef({}); // hold all of the actual data
|
||||
@@ -215,7 +216,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
token: 0,
|
||||
});
|
||||
const autoTaskLsOnEmptyDirectoriesRef = React.useRef(false);
|
||||
const taskingData = React.useRef({"parameters": "", "ui_feature": "file_browser:list"});
|
||||
const taskingData = React.useRef({"parameters": "", "ui_feature": baseUIFeature +":list"});
|
||||
const mountedRef = React.useRef(true);
|
||||
const tableOpenedPathIdRef = React.useRef({
|
||||
host: "",
|
||||
@@ -255,6 +256,10 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
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
|
||||
@@ -324,9 +329,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
}
|
||||
existingData.comment += data.data.mythictree_stream[i].comment;
|
||||
existingData.tags = [...existingData.tags, ...data.data.mythictree_stream[i].tags];
|
||||
if(!existingData.metadata.has_children){
|
||||
existingData.metadata.has_children = data.data.mythictree_stream[i].metadata.has_children;
|
||||
}
|
||||
existingData.has_children = data.data.mythictree_stream[i].has_children || existingData.has_children;
|
||||
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];
|
||||
@@ -464,9 +467,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
}
|
||||
existingData.comment += mythictree[i].comment;
|
||||
existingData.tags = [...existingData.tags, ...mythictree[i].tags];
|
||||
if(!existingData.metadata.has_children){
|
||||
existingData.metadata.has_children = mythictree[i].metadata.has_children;
|
||||
}
|
||||
existingData.has_children = mythictree[i].has_children || existingData.has_children;
|
||||
//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)){
|
||||
@@ -575,7 +576,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
taskingData.current = ({
|
||||
"token": token,
|
||||
"parameters": {path: path, full_path: path, host: selectedFolderData.host, file: ""},
|
||||
"ui_feature": "file_browser:list", callback_id, callback_display_id});
|
||||
"ui_feature": baseUIFeature + ":list", callback_id, callback_display_id});
|
||||
setOpenTaskingButton(true);
|
||||
};
|
||||
const onListFilesButtonFromTableWithNoEntries = () => {
|
||||
@@ -587,7 +588,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
host: selectedFolderData.host,
|
||||
file: ""
|
||||
},
|
||||
"ui_feature": "file_browser:list",
|
||||
"ui_feature": baseUIFeature +":list",
|
||||
callback_id: tabInfo.callbackID,
|
||||
callback_display_id: tabInfo.displayID});
|
||||
setOpenTaskingButton(true);
|
||||
@@ -596,7 +597,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
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);
|
||||
@@ -656,7 +657,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
taskingData.current = ({
|
||||
"token": localSelectedToken.current,
|
||||
"parameters": {path: nodeData.full_path_text, full_path: nodeData.full_path_text, host: nodeData.host, file: ""},
|
||||
"ui_feature": "file_browser:list", callback_id, callback_display_id});
|
||||
"ui_feature": baseUIFeature + ":list", callback_id, callback_display_id});
|
||||
setOpenTaskingButton(true);
|
||||
}
|
||||
const openDirectoryPath = ({group, host, path}) => {
|
||||
@@ -690,6 +691,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
taskListing={taskListing}
|
||||
tableOpenedPathId={tableOpenedPathIdRef.current}
|
||||
getLoadedCommandForUIFeature={getLoadedCommandForUIFeature}
|
||||
baseUIFeature={baseUIFeature}
|
||||
/>
|
||||
|
||||
</div>
|
||||
@@ -708,6 +710,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
fetchParentFolderData={fetchParentFolderData}
|
||||
fetchFolderData={fetchFolderData}
|
||||
openDirectoryPath={openDirectoryPath}
|
||||
baseUIFeature={baseUIFeature}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flexGrow: 1 }}>
|
||||
@@ -727,6 +730,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
onTaskRowActions={onTaskRowActions}
|
||||
getLoadedCommandForUIFeature={getLoadedCommandForUIFeature}
|
||||
me={me}
|
||||
baseUIFeature={baseUIFeature}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -758,7 +762,8 @@ const FileBrowserTableTop = ({
|
||||
fetchParentFolderData,
|
||||
fetchFolderData,
|
||||
openDirectoryPath,
|
||||
tabInfo
|
||||
tabInfo,
|
||||
baseUIFeature
|
||||
}) => {
|
||||
const autoTaskLsOnEmptyDirectories = useGetMythicSetting({
|
||||
setting_name: "autoTaskLsOnEmptyDirectories", default_value: false
|
||||
@@ -794,6 +799,9 @@ const FileBrowserTableTop = ({
|
||||
shouldResubscribe: true,
|
||||
onData: ({data}) => {
|
||||
setTokenOptions(data.data.callbacktoken);
|
||||
if(data.data.callbacktoken.length === 0) {
|
||||
onChangeSelectedToken("Default Token");
|
||||
}
|
||||
}
|
||||
});
|
||||
useEffect(() => {
|
||||
@@ -964,9 +972,10 @@ const FileBrowserTableTop = ({
|
||||
borderRight: "1px solid grey",
|
||||
marginRight: "10px",
|
||||
padding: "0 5px 0 0"}}>
|
||||
{tokenOptions.length > 0 ? (
|
||||
<CallbacksTabsTaskingInputTokenSelect options={tokenOptions} changeSelectedToken={changeSelectedToken}/>
|
||||
) : null}
|
||||
{tokenOptions.length > 0 &&
|
||||
<CallbacksTabsTaskingInputTokenSelect width={"100%"}
|
||||
options={tokenOptions} changeSelectedToken={changeSelectedToken}/>
|
||||
}
|
||||
<MythicStyledTooltip title={`Move back to previous listing`}>
|
||||
<IconButton style={{ padding: '3px' }}
|
||||
disabled={historyIndex >= history.length -1 }
|
||||
|
||||
@@ -188,10 +188,15 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
}
|
||||
return tempData;
|
||||
}, [allData, sortData]);
|
||||
const onSubmitFilterOptions = (newFilterOptions) => {
|
||||
setFilterOptions(newFilterOptions);
|
||||
const onSubmitFilterOptions = (value) => {
|
||||
setFilterOptions({...filterOptions, [selectedColumn.key]: value });
|
||||
}
|
||||
const filterRow = (row) => {
|
||||
if(props.treeRootData[props.selectedFolderData.group] === undefined ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host] === undefined ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row] === undefined) {
|
||||
return true;
|
||||
}
|
||||
if(!props.showDeletedFiles && props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.deleted){
|
||||
return true;
|
||||
}
|
||||
@@ -216,7 +221,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
treeRootData={props.treeRootData[props.selectedFolderData.group]}
|
||||
selectedFolderData={props.selectedFolderData}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}
|
||||
cellData={row}
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.name_text}
|
||||
me={props.me}
|
||||
tabInfo={props.tabInfo}
|
||||
onTaskRowAction={props.onTaskRowAction} />;
|
||||
@@ -224,7 +229,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
return <FileBrowserTableRowNameCell
|
||||
treeRootData={props.treeRootData[props.selectedFolderData.group]}
|
||||
selectedFolderData={props.selectedFolderData}
|
||||
cellData={row}
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.name_text}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]} />;
|
||||
case "Size":
|
||||
return <TableRowSizeCell cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.metadata?.size}
|
||||
@@ -254,9 +259,9 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
return "showTask";
|
||||
}else if(sortedData.length > 0 || props?.selectedFolderData?.success){
|
||||
return "normal";
|
||||
} else if(sortedData.length === 0 && props?.selectedFolderData?.metadata?.has_children && props?.selectedFolderData?.success === null){
|
||||
} else if(sortedData.length === 0 && props?.selectedFolderData?.has_children && props?.selectedFolderData?.success === null){
|
||||
return "fetchLocal";
|
||||
}else if(sortedData.length === 0 && !props?.selectedFolderData?.metadata?.has_children && props?.selectedFolderData?.success === null) {
|
||||
}else if(sortedData.length === 0 && !props?.selectedFolderData?.has_children && props?.selectedFolderData?.success === null) {
|
||||
return "fetchRemote";
|
||||
}else if(sortedData.length === 0){
|
||||
return "fetchRemote";
|
||||
@@ -304,7 +309,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
};
|
||||
const contextMenuOptions = [
|
||||
{
|
||||
name: 'Filter Column',
|
||||
name: 'Filter Column', type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
@@ -319,7 +324,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Show/Hide Columns",
|
||||
name: "Show/Hide Columns",type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
@@ -365,7 +370,9 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
copyStringToClipboard(element.name_text);
|
||||
if(copyStringToClipboard(element.name_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -373,22 +380,26 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
copyStringToClipboard(element.full_path_text);
|
||||
if(copyStringToClipboard(element.full_path_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Attributes', type: "item",
|
||||
name: 'Metadata', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
copyStringToClipboard(JSON.stringify(element?.metadata?.permissions, null, 2));
|
||||
if(copyStringToClipboard(JSON.stringify(element?.metadata?.permissions, null, 2))){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
async function optionsB (callback_id, callback_display_id, element) {
|
||||
let downloadCommand = await props.getLoadedCommandForUIFeature(callback_id, element.can_have_children ? "file_browser:download_folder" : "file_browser:download");
|
||||
let downloadCommand = await props.getLoadedCommandForUIFeature(callback_id, element.can_have_children ? props.baseUIFeature + ":download_folder" : props.baseUIFeature + ":download");
|
||||
let downloadDisplay = "Download (Unsupported)";
|
||||
if(element.can_have_children){
|
||||
downloadDisplay = "Download Folder (Unsupported)";
|
||||
@@ -399,12 +410,12 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
downloadDisplay = `Download Folder (${downloadCommand.command.cmd})`;
|
||||
}
|
||||
}
|
||||
let listCommand = await props.getLoadedCommandForUIFeature(callback_id, "file_browser:list");
|
||||
let listCommand = await props.getLoadedCommandForUIFeature(callback_id, props.baseUIFeature + ":list");
|
||||
let listDisplay = "List (Unsupported)";
|
||||
if(listCommand !== undefined){
|
||||
listDisplay = `List (${listCommand.command.cmd})`;
|
||||
}
|
||||
let removeCommand = await props.getLoadedCommandForUIFeature(callback_id, "file_browser:remove");
|
||||
let removeCommand = await props.getLoadedCommandForUIFeature(callback_id, props.baseUIFeature + ":remove");
|
||||
let removeDisplay = "Remove (Unsupported)";
|
||||
if(removeCommand !== undefined){
|
||||
removeDisplay = `Remove (${removeCommand.command.cmd})`;
|
||||
@@ -421,7 +432,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: element.can_have_children ? 'file_browser:download_folder' : 'file_browser:download',
|
||||
uifeature: element.can_have_children ? props.baseUIFeature + ':download_folder' : props.baseUIFeature + ':download',
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
},
|
||||
@@ -436,7 +447,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: 'file_browser:list',
|
||||
uifeature: props.baseUIFeature + ':list',
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
},
|
||||
@@ -451,7 +462,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: 'file_browser:remove',
|
||||
uifeature: props.baseUIFeature + ':remove',
|
||||
getConfirmation: true,
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
@@ -476,7 +487,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
full_path: selectedRows[i].full_path_text,
|
||||
host: selectedRows[i].host,
|
||||
filename: selectedRows[i].name_text,
|
||||
uifeature: selectedRows[i].can_have_children ? 'file_browser:download_folder': 'file_browser:download',
|
||||
uifeature: selectedRows[i].can_have_children ? props.baseUIFeature + ':download_folder': props.baseUIFeature + ':download',
|
||||
callback_id: props.tabInfo.callbackID,
|
||||
callback_display_id: props.tabInfo.displayID
|
||||
});
|
||||
@@ -496,7 +507,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
full_path: selectedRows[i].full_path_text,
|
||||
host: selectedRows[i].host,
|
||||
filename: selectedRows[i].name_text,
|
||||
uifeature: 'file_browser:remove',
|
||||
uifeature: props.baseUIFeature + ':remove',
|
||||
getConfirmation: true,
|
||||
callback_id: props.tabInfo.callbackID,
|
||||
callback_display_id: props.tabInfo.displayID
|
||||
@@ -517,7 +528,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
full_path: selectedRows[i].full_path_text,
|
||||
host: selectedRows[i].host,
|
||||
filename: selectedRows[i].name_text,
|
||||
uifeature: 'file_browser:list',
|
||||
uifeature: props.baseUIFeature + ':list',
|
||||
callback_id: props.tabInfo.callbackID,
|
||||
callback_display_id: props.tabInfo.displayID
|
||||
});
|
||||
@@ -619,15 +630,18 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
<RenderSingleTask task_id={props.selectedFolderData?.task_id} />
|
||||
}
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xs" open={openContextMenu}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
innerDialog={<TableFilterDialog
|
||||
selectedColumn={selectedColumn}
|
||||
filterOptions={filterOptions}
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
onClose={()=>{setOpenContextMenu(false);}} />}
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={filterOptions[selectedColumn.key]}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
{openAdjustColumnsDialog &&
|
||||
@@ -646,8 +660,8 @@ const FileBrowserTableRowNameCell = ({cellData, rowData, treeRootData, selected
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div style={{ alignItems: 'center', display: 'flex', maxHeight: "100%", textDecoration: treeRootData[selectedFolderData.host][cellData]?.deleted ? 'line-through' : '' }}>
|
||||
{!treeRootData[selectedFolderData.host][cellData]?.can_have_children ? (
|
||||
<div style={{ alignItems: 'center', display: 'flex', maxHeight: "100%", textDecoration: treeRootData[selectedFolderData.host][rowData.full_path_text]?.deleted ? 'line-through' : '' }}>
|
||||
{!treeRootData[selectedFolderData.host][rowData.full_path_text]?.can_have_children ? (
|
||||
<DescriptionIcon size={"small"} style={{
|
||||
marginRight: '5px' }} />
|
||||
) : (
|
||||
@@ -658,7 +672,7 @@ const FileBrowserTableRowNameCell = ({cellData, rowData, treeRootData, selected
|
||||
marginRight: '8px',
|
||||
marginLeft: "4px",
|
||||
color:
|
||||
treeRootData[selectedFolderData.host][cellData]?.success || treeRootData[selectedFolderData.host][cellData]?.metadata?.has_children
|
||||
treeRootData[selectedFolderData.host][rowData.full_path_text]?.success || treeRootData[selectedFolderData.host][rowData.full_path_text]?.has_children
|
||||
? theme.folderColor
|
||||
: 'grey',
|
||||
}}
|
||||
@@ -667,17 +681,17 @@ const FileBrowserTableRowNameCell = ({cellData, rowData, treeRootData, selected
|
||||
<pre
|
||||
style={{
|
||||
color:
|
||||
treeRootData[selectedFolderData.host][cellData]?.success !== null
|
||||
treeRootData[selectedFolderData.host][rowData.full_path_text]?.success !== null
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary,
|
||||
}}>
|
||||
{treeRootData[selectedFolderData.host][cellData]?.name_text}
|
||||
{cellData}
|
||||
</pre>
|
||||
{treeRootData[selectedFolderData.host][cellData]?.success === true ? (
|
||||
{treeRootData[selectedFolderData.host][rowData.full_path_text]?.success === true ? (
|
||||
<MythicStyledTooltip title='Successfully listed contents of folder' tooltipStyle={{display: "inline-flex", marginLeft: "5px"}}>
|
||||
<CheckCircleOutlineIcon color="success" fontSize='small' />
|
||||
</MythicStyledTooltip>
|
||||
) : treeRootData[selectedFolderData.host][cellData]?.success === false ? (
|
||||
) : treeRootData[selectedFolderData.host][rowData.full_path_text]?.success === false ? (
|
||||
<MythicStyledTooltip title='Failed to list contents of folder' tooltipStyle={{display: "inline-flex", marginLeft: "5px"}}>
|
||||
<ErrorIcon fontSize='small' color="error" />
|
||||
</MythicStyledTooltip>
|
||||
@@ -810,7 +824,7 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
|
||||
};
|
||||
const optionsA = [
|
||||
{
|
||||
name: treeRootData[selectedFolderData.host][cellData]?.name_text, icon: null, click: ({event}) => {},
|
||||
name: treeRootData[selectedFolderData.host][rowData.full_path_text]?.name_text, icon: null, click: ({event}) => {},
|
||||
type: "item", disabled: true
|
||||
},
|
||||
{
|
||||
@@ -818,7 +832,7 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
|
||||
icon: <VisibilityIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
setPermissionData(treeRootData[selectedFolderData.host][cellData].metadata);
|
||||
setPermissionData(treeRootData[selectedFolderData.host][rowData.full_path_text].metadata);
|
||||
setViewPermissionsDialogOpen(true);
|
||||
},
|
||||
},
|
||||
@@ -828,7 +842,7 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
getHistory({ variables: {
|
||||
full_path_text: treeRootData[selectedFolderData.host][cellData].full_path_text,
|
||||
full_path_text: treeRootData[selectedFolderData.host][rowData.full_path_text].full_path_text,
|
||||
host: selectedFolderData.host,
|
||||
group: [selectedFolderData.group],
|
||||
} });
|
||||
@@ -867,8 +881,8 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
|
||||
ref={dropdownAnchorRef}>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
{treeRootData[selectedFolderData.host][cellData]?.filemeta.length > 0 ?
|
||||
<MythicStyledTooltip title={treeRootData[selectedFolderData.host][cellData]?.filemeta[0]?.complete ?
|
||||
{treeRootData[selectedFolderData.host][rowData.full_path_text]?.filemeta.length > 0 ?
|
||||
<MythicStyledTooltip title={treeRootData[selectedFolderData.host][rowData.full_path_text]?.filemeta[0]?.complete ?
|
||||
"Preview Media" : "Preview Partial Media"}>
|
||||
<FontAwesomeIcon icon={faPhotoVideo} size={"1x"} style={{
|
||||
position: "relative", cursor: "pointer", display: "inline-block"}}
|
||||
@@ -880,8 +894,8 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openPreviewMediaDialog}
|
||||
onClose={(e)=>{setOpenPreviewMediaDialog(false);}}
|
||||
innerDialog={<PreviewFileMediaDialog
|
||||
agent_file_id={treeRootData[selectedFolderData.host][cellData]?.filemeta[0]?.agent_file_id}
|
||||
filename={b64DecodeUnicode(treeRootData[selectedFolderData.host][cellData]?.filemeta[0]?.filename_text)}
|
||||
agent_file_id={treeRootData[selectedFolderData.host][rowData.full_path_text]?.filemeta[0]?.agent_file_id}
|
||||
filename={b64DecodeUnicode(treeRootData[selectedFolderData.host][rowData.full_path_text]?.filemeta[0]?.filename_text)}
|
||||
onClose={(e)=>{setOpenPreviewMediaDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -6,11 +6,14 @@ import ListIcon from '@mui/icons-material/List';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import {getAllParentNodes} from "./CallbacksTabsFileBrowser";
|
||||
import {Dropdown, DropdownMenuItem, DropdownNestedMenuItem} from "../../MythicComponents/MythicNestedMenus";
|
||||
import {copyStringToClipboard} from "../../utilities/Clipboard";
|
||||
import FileCopyOutlinedIcon from '@mui/icons-material/FileCopyOutlined';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
|
||||
export const getOpenIDFromNode = (node) => {
|
||||
return `${node.group};${node.host};${node.full_path_text}`;
|
||||
}
|
||||
export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatrix, fetchFolderData, setTableData, taskListing, tableOpenedPathId, showDeletedFiles, tabInfo, selectedFolderData}) => {
|
||||
export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatrix, fetchFolderData, setTableData, taskListing, tableOpenedPathId, showDeletedFiles, tabInfo, selectedFolderData, baseUIFeature}) => {
|
||||
const [openNodes, setOpenNodes] = React.useState({});
|
||||
const groupName = React.useRef("");
|
||||
const [openViewGroupsDialog, setOpenViewGroupDialog] = React.useState(false);
|
||||
@@ -19,7 +22,7 @@ export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatri
|
||||
//console.log("toggleNodeExpanded", nodeId, nodeData);
|
||||
let lastOpenedNodeNewState = true;
|
||||
let lastOpenedNodeID = getOpenIDFromNode(lastOpenedNodeRef.current);
|
||||
if(!lastOpenedNodeRef.current?.metadata?.has_children){
|
||||
if(!lastOpenedNodeRef.current?.has_children){
|
||||
lastOpenedNodeNewState = false;
|
||||
}
|
||||
lastOpenedNodeRef.current = {...nodeData};
|
||||
@@ -28,7 +31,7 @@ export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatri
|
||||
setOpenNodes({
|
||||
...openNodes,
|
||||
[getOpenIDFromNode(nodeData)]: true,
|
||||
[lastOpenedNodeID]: lastOpenedNodeNewState
|
||||
//[lastOpenedNodeID]: lastOpenedNodeNewState
|
||||
});
|
||||
};
|
||||
const toggleNodeCollapsed = (nodeId, nodeData) => {
|
||||
@@ -77,6 +80,41 @@ export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatri
|
||||
});
|
||||
}, [selectedFolderData]);
|
||||
const contextMenuOptions= (callback_id, callback_display_id, node) => [
|
||||
{
|
||||
name: "Copy to Clipboard", icon: null, click: () => {}, type: "menu",
|
||||
menuItems: [
|
||||
{
|
||||
name: 'Name', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(node.name_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Full Path', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(node.full_path_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Metadata', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(JSON.stringify(node?.metadata, null, 2))){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'List', type: "item", icon: <ListIcon color="warning" style={{ paddingRight: '5px'}} />,
|
||||
click: ({event}) => {
|
||||
@@ -146,6 +184,7 @@ export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatri
|
||||
onCollapseNode={toggleNodeCollapsed}
|
||||
onContextMenu={onContextMenu}
|
||||
tabInfo={tabInfo}
|
||||
baseUIFeature={baseUIFeature}
|
||||
/>
|
||||
{openViewGroupsDialog &&
|
||||
<MythicDialog
|
||||
|
||||
@@ -384,8 +384,8 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
}
|
||||
return tempData;
|
||||
}, [allData, sortData]);
|
||||
const onSubmitFilterOptions = (newFilterOptions) => {
|
||||
setFilterOptions(newFilterOptions);
|
||||
const onSubmitFilterOptions = (value) => {
|
||||
setFilterOptions({...filterOptions, [selectedColumn.current.key]: value });
|
||||
if(viewSingleTreeData){
|
||||
return
|
||||
}
|
||||
@@ -605,7 +605,7 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
};
|
||||
const contextMenuOptions = [
|
||||
{
|
||||
name: 'Filter Column',
|
||||
name: 'Filter Column', type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
@@ -620,7 +620,7 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Show/Hide Columns",
|
||||
name: "Show/Hide Columns", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
@@ -661,13 +661,18 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
/>
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xs" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={<TableFilterDialog
|
||||
selectedColumn={selectedColumn.current}
|
||||
filterOptions={filterOptions}
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
onClose={()=>{setOpenContextMenu(false);}} />}
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={filterOptions[selectedColumn.current]}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
{openAdjustColumnsDialog &&
|
||||
|
||||
@@ -284,6 +284,9 @@ export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
return;
|
||||
}
|
||||
tokenOptions.current = data.data.callbacktoken;
|
||||
if(tokenOptions.current.length === 0) {
|
||||
props.changeSelectedToken("Default Token");
|
||||
}
|
||||
}
|
||||
});
|
||||
useSubscription(contextSubscription, {
|
||||
@@ -789,7 +792,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
str.split('').forEach((value, i, s) => {
|
||||
//loop over every value in the string
|
||||
//console.log(value);
|
||||
if(value === "\\"){
|
||||
if( (sQuoted || dQuoted) && value === "\\"){
|
||||
if(!backslash){
|
||||
backslash = true;
|
||||
return;
|
||||
@@ -1794,11 +1797,11 @@ export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
innerDialog={<MythicSelectFromListDialog onClose={() => {
|
||||
setOpenSelectCommandDialog(false);
|
||||
}}
|
||||
onSubmit={processCommandAndCommandLine}
|
||||
options={commandOptions.current}
|
||||
title={"Select Command"}
|
||||
action={"select"} identifier={"id"}
|
||||
display={"display"}/>}
|
||||
onSubmit={processCommandAndCommandLine}
|
||||
options={commandOptions.current}
|
||||
title={"Select Command"}
|
||||
action={"select"} identifier={"id"}
|
||||
display={"display"}/>}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ export function CallbacksTabsTaskingInputTokenSelect(props) {
|
||||
}
|
||||
}
|
||||
return (
|
||||
<FormControl style={{width: "20%"}}>
|
||||
<FormControl style={{width: props.width ? props.width : "20%"}}>
|
||||
<Select
|
||||
labelId="demo-dialog-select-label"
|
||||
id="demo-dialog-select"
|
||||
|
||||
@@ -414,7 +414,8 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
|
||||
}, [taskingData, newlyIssuedTasks]);
|
||||
return (
|
||||
<MythicTabPanel index={index} value={value} >
|
||||
<Split direction="horizontal" minSize={[0,0]} style={{width: "100%", height: "100%", overflowY: "auto", display: "flex"}} sizes={[30, 70]} >
|
||||
<Split direction="horizontal" minSize={[0,0]} sizes={[30, 70]}
|
||||
style={{width: "100%", height: "100%", overflowY: "auto", display: "flex", justifyContent: "flex-end"}} >
|
||||
<div className="bg-gray-base" style={{display: "inline-flex", flexDirection: "column"}} >
|
||||
|
||||
<div style={{overflowY: "auto", flexGrow: 1}} id={`taskingPanelSplit${tabInfo.callbackID}`}>
|
||||
|
||||
@@ -384,7 +384,7 @@ export function CallbacksTop(props){
|
||||
callbackEdges.current = updated;
|
||||
}
|
||||
});
|
||||
const onOpenTabLocal = React.useCallback( ({tabType, tabID, callbackID}) => {
|
||||
const onOpenTabLocal = React.useCallback( ({tabType, tabID, callbackID, customBrowser}) => {
|
||||
for(let i = 0; i < callbacks.current.length; i++){
|
||||
if(callbacks.current[i]["id"] === callbackID){
|
||||
const tabData = {tabID, tabType, callbackID,
|
||||
@@ -396,7 +396,9 @@ export function CallbacksTop(props){
|
||||
callbackDescription: callbacks.current[i]["description"],
|
||||
host: callbacks.current[i]["host"],
|
||||
color: callbacks.current[i]["color"],
|
||||
os: callbacks.current[i]["payload"]["os"]};
|
||||
os: callbacks.current[i]["payload"]["os"],
|
||||
customBrowser: customBrowser,
|
||||
};
|
||||
props.onOpenTab(tabData);
|
||||
}
|
||||
}
|
||||
@@ -418,7 +420,9 @@ export function CallbacksTop(props){
|
||||
callbackDescription: callbacks.current[i]["description"],
|
||||
host: callbacks.current[i]["host"],
|
||||
color: callbacks.current[i]["color"],
|
||||
os: callbacks.current[i]["payload"]["os"]};
|
||||
os: callbacks.current[i]["payload"]["os"],
|
||||
customBrowser: tabs[j].customBrowser,
|
||||
};
|
||||
newTabData.push(tabData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,7 +389,7 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
<div style={{
|
||||
display: "flex", overflowY: "auto",
|
||||
position: "relative", height: props.expand ? "100%" : undefined, maxHeight: props.expand ? "100%" : "500px",
|
||||
flexDirection: "column"
|
||||
flexDirection: "column", width: "100%"
|
||||
}}>
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute",}} invisible={false}>
|
||||
<div style={{
|
||||
|
||||
@@ -41,6 +41,7 @@ import 'ace-builds/src-noconflict/mode-apache_conf';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import TabContext from '@mui/lab/TabContext';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import {previewFileQuery} from "../Search/FileMetaTable";
|
||||
import { useMutation, gql, useQuery } from '@apollo/client';
|
||||
@@ -48,12 +49,20 @@ import CodeIcon from '@mui/icons-material/Code';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import {b64DecodeUnicode} from "./ResponseDisplay";
|
||||
import {MythicDialog, TableRowSizeCell} from "../../MythicComponents/MythicDialog";
|
||||
import {Table, TableHead, TableRow, TableBody} from '@mui/material';
|
||||
import {Table, TableHead, TableRow, TableBody, TableCell, Paper} from '@mui/material';
|
||||
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
|
||||
import WarningOutlinedIcon from '@mui/icons-material/WarningOutlined';
|
||||
import {TagsDisplay, ViewEditTags} from "../../MythicComponents/MythicTag";
|
||||
import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery";
|
||||
import {ResponseDisplayScreenshotModal} from "./ResponseDisplayScreenshotModal";
|
||||
import initSQLJS from 'sql.js';
|
||||
import TabPanel from '@mui/lab/TabPanel';
|
||||
import TabList from '@mui/lab/TabList';
|
||||
import {Backdrop, CircularProgress} from '@mui/material';
|
||||
// Required to let webpack 4 know it needs to copy the wasm file to our assets
|
||||
// eslint-disable-next-line import/no-webpack-loader-syntax
|
||||
import sqlWasm from "!!file-loader?name=sql-wasm-[contenthash].wasm!sql.js/dist/sql-wasm.wasm";
|
||||
import {copyStringToClipboard} from "../../utilities/Clipboard";
|
||||
|
||||
export const modeOptions = ["csharp", "golang", "html", "json", "markdown", "ruby", "python", "java",
|
||||
"javascript", "yaml", "toml", "swift", "sql", "rust", "powershell", "pgsql", "perl", "php", "objectivec",
|
||||
@@ -121,7 +130,6 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
<div style={{display: "flex", height: "100%", flexDirection: "column"}}>
|
||||
<DisplayFileMetaData fileMetaData={fileMetaData} />
|
||||
<AppBar color={'default'} position='static' className={"no-box-shadow"}>
|
||||
|
||||
<Tabs
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
@@ -139,6 +147,7 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
<Tab className={value === 0 ? "selectedCallback": ""} label={"Preview"}></Tab>
|
||||
<Tab className={value === 1 ? "selectedCallback": ""} label={"Text"}></Tab>
|
||||
<Tab className={value === 2 ? "selectedCallback": ""} label={"Hex"}></Tab>
|
||||
<Tab className={value === 3 ? "selectedCallback": ""} label={"Database"}></Tab>
|
||||
<MythicStyledTooltip title={"Download the file"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<Button style={{}} size={"small"} href={"/direct/download/" + media.agent_file_id}
|
||||
download color={"success"}>
|
||||
@@ -147,7 +156,7 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
</MythicStyledTooltip>
|
||||
</Tabs>
|
||||
</AppBar>
|
||||
<div hidden={value !== 0} style={{height: "100%"}} role='tabpanel' >
|
||||
<div hidden={value !== 0} style={{height: "100%", overflow: "auto"}} role='tabpanel' >
|
||||
{value === 0 &&
|
||||
<DisplayMedia agent_file_id={media?.agent_file_id || ""}
|
||||
task={task} filename={media?.filename || undefined}
|
||||
@@ -155,7 +164,7 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
expand={expand} />
|
||||
}
|
||||
</div>
|
||||
<div hidden={value !== 1} style={{height: "100%"}} role='tabpanel' >
|
||||
<div hidden={value !== 1} style={{height: "100%", overflow: "auto"}} role='tabpanel' >
|
||||
{value === 1 &&
|
||||
<DisplayText agent_file_id={media?.agent_file_id || ""}
|
||||
task={task} filename={media?.filename || undefined}
|
||||
@@ -163,7 +172,7 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
expand={expand} preview />
|
||||
}
|
||||
</div>
|
||||
<div hidden={value !== 2} style={{height: "100%"}} role='tabpanel' >
|
||||
<div hidden={value !== 2} style={{height: "100%", overflow: "auto"}} role='tabpanel' >
|
||||
{value === 2 &&
|
||||
<DisplayHex agent_file_id={media?.agent_file_id || ""}
|
||||
task={task} filename={media?.filename || undefined}
|
||||
@@ -172,6 +181,15 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
}
|
||||
|
||||
</div>
|
||||
<div hidden={value !== 3} style={{height: "100%", overflow: "auto"}} role='tabpanel' >
|
||||
{value === 3 &&
|
||||
<DisplayDatabase agent_file_id={media?.agent_file_id || ""}
|
||||
task={task} filename={media?.filename || undefined}
|
||||
fileMetaData={fileMetaData}
|
||||
expand={expand} />
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -353,7 +371,7 @@ export const DisplayMedia = ({agent_file_id, filename, expand, task, fileMetaDat
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const MaxRenderSize = 2000000;
|
||||
const MaxRenderSize = 2000000; // 2MB
|
||||
const DisplayFileMetaData = ({fileMetaData}) => {
|
||||
return (
|
||||
<Table style={{marginLeft: "0px", width: "100%", tableLayout: "fixed"}}>
|
||||
@@ -634,4 +652,258 @@ const DisplayHex = ({agent_file_id, expand, fileMetaData}) => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const MAX_ROWS = 1000;
|
||||
const DisplayDatabase = ({agent_file_id, expand, fileMetaData}) => {
|
||||
const theme = useTheme();
|
||||
const queryCountRef = React.useRef(1);
|
||||
const currentContentRef = React.useRef();
|
||||
const [selectedTab, setSelectedTab] = React.useState(0);
|
||||
const [sql, setSql] = React.useState(null);
|
||||
const [query, setQuery] = React.useState("select * from cookies;");
|
||||
const [results, setResults] = React.useState([]);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const onChangeText = (data) => {
|
||||
setQuery(data);
|
||||
}
|
||||
const removeQuery = (event, index) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if(selectedTab === index){
|
||||
if(index !== 0){
|
||||
setSelectedTab(index-1);
|
||||
}
|
||||
}
|
||||
setResults(results.toSpliced(index, 1));
|
||||
}
|
||||
const onSubmitQuery = (event) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
if(sql){
|
||||
try{
|
||||
const result = sql.exec(query);
|
||||
//console.log(result);
|
||||
if(result.length > 0){
|
||||
setResults([...results, {name: `Query: ${queryCountRef.current}`, query: query, results: result[0]}]);
|
||||
setSelectedTab(queryCountRef.current - 1);
|
||||
queryCountRef.current += 1;
|
||||
}
|
||||
}catch(e){
|
||||
console.log(e);
|
||||
snackActions.error(e.message);
|
||||
}
|
||||
} else {
|
||||
console.log(sql);
|
||||
snackActions.error("Failed to load as SQL Database");
|
||||
}
|
||||
}
|
||||
const onChangeTab = (_, v) => {
|
||||
const tabQuery = results[v]?.query;
|
||||
setQuery(tabQuery);
|
||||
setSelectedTab(v);
|
||||
}
|
||||
const onCopyToClipboard = (data) => {
|
||||
let result = copyStringToClipboard(data);
|
||||
if(result){
|
||||
snackActions.success("Copied text!");
|
||||
}else{
|
||||
snackActions.error("Failed to copy text");
|
||||
}
|
||||
}
|
||||
const onSaveOutputCSV = (result) => {
|
||||
const filteredData = result.results.values;
|
||||
let outputHeaders = "";
|
||||
let outputRow = "";
|
||||
outputHeaders = result.results.columns.join(",");
|
||||
for(let i = 0; i < filteredData.length; i++){
|
||||
outputRow += filteredData[i].join(",") + "\n"
|
||||
}
|
||||
onCopyToClipboard(outputHeaders + "\n" + outputRow);
|
||||
}
|
||||
const onSaveOutputPrettyPrint = (result) => {
|
||||
let tableData = result.results.values;
|
||||
if(tableData.length === 0){return}
|
||||
let outputHeaders = result.results.columns;
|
||||
let outputHeadersMaxLength = outputHeaders.reduce( (prev, cur) => {
|
||||
return [...prev, cur.length]
|
||||
}, []);
|
||||
tableData.map( cur => {
|
||||
for(let i = 0; i < outputHeaders.length; i++){
|
||||
if(outputHeadersMaxLength[i] < String(cur[i]).length){
|
||||
outputHeadersMaxLength[i] = String(cur[i]).length;
|
||||
}
|
||||
}
|
||||
});
|
||||
let allOutput = "";
|
||||
for(let i = 0; i < outputHeaders.length; i++){
|
||||
let remainingSpace = Math.max(0, outputHeadersMaxLength[i] - outputHeaders[i].length);
|
||||
allOutput += " " + outputHeaders[i].toUpperCase() + " ".repeat(remainingSpace + 2);
|
||||
}
|
||||
allOutput += "\n";
|
||||
for(let i = 0; i < outputHeaders.length; i++){
|
||||
let remainingSpace = Math.max(0, outputHeadersMaxLength[i] - outputHeaders[i].length);
|
||||
allOutput += " " + "-".repeat(outputHeaders[i].length) + " ".repeat(remainingSpace + 2);
|
||||
}
|
||||
allOutput += "\n";
|
||||
for(let i = 0; i < tableData.length; i++){
|
||||
for(let j = 0; j < outputHeaders.length; j++){
|
||||
let remainingSpace = Math.max(0, outputHeadersMaxLength[j] - String(tableData[i][j]).length);
|
||||
allOutput += "| " + String(tableData[i][j]) + " ".repeat(remainingSpace) + " ";
|
||||
}
|
||||
allOutput += " |\n"
|
||||
}
|
||||
onCopyToClipboard(allOutput);
|
||||
}
|
||||
React.useEffect( () => {
|
||||
async function initialize(){
|
||||
const newSQL = await initSQLJS({locateFile: () => sqlWasm});
|
||||
fetch('/direct/view/' + agent_file_id).then((response) => {
|
||||
if(response.status !== 200){
|
||||
setLoading(false);
|
||||
snackActions.warning("Failed to fetch contents from Mythic");
|
||||
return;
|
||||
}
|
||||
response.arrayBuffer().then( data => {
|
||||
const SQL = new newSQL.Database(new Uint8Array(data));
|
||||
if(SQL){
|
||||
setSql(SQL);
|
||||
}
|
||||
setLoading(false);
|
||||
}).catch(error => {
|
||||
setLoading(false);
|
||||
snackActions.warning("Error getting contents from server: " + error.toString());
|
||||
console.log("Error trying to get json response", error, response);
|
||||
});
|
||||
}).catch(error => {
|
||||
if(error.toString() === "TypeError: Failed to fetch"){
|
||||
snackActions.warning("Please refresh and accept the SSL connection error");
|
||||
} else {
|
||||
snackActions.warning("Error talking to server: " + error.toString());
|
||||
}
|
||||
setLoading(false);
|
||||
console.log("There was an error!", error);
|
||||
});
|
||||
}
|
||||
initialize();
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
// Execute query on Ctrl+Enter or Cmd+Enter when in the SQL editor
|
||||
if (e.shiftKey && e.key === 'Enter') {
|
||||
onSubmitQuery(e);
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [onSubmitQuery]);
|
||||
return (
|
||||
<div style={{display: "flex", height: "100%", flexDirection: "column", width: "100%", position: "relative"}}>
|
||||
<Backdrop open={loading} onClick={()=>{setLoading(false);}} style={{zIndex: 2, position: "absolute"}}>
|
||||
<CircularProgress color="inherit" disableShrink />
|
||||
</Backdrop>
|
||||
<div style={{display: "inline-flex", flexDirection: "row", alignItems: "center", borderBottom: "1px solid grey"}}>
|
||||
<AceEditor
|
||||
className={"roundedBottomCorners"}
|
||||
ref={currentContentRef}
|
||||
mode={'sql'}
|
||||
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
|
||||
fontSize={14}
|
||||
showGutter={ true}
|
||||
onChange={onChangeText}
|
||||
highlightActiveLine={false}
|
||||
showPrintMargin={false}
|
||||
value={query}
|
||||
height={"100%"}
|
||||
maxLines={3}
|
||||
width={"100%"}
|
||||
wrapEnabled={true}
|
||||
minLines={2}
|
||||
setOptions={{
|
||||
showLineNumbers: true,
|
||||
tabSize: 4,
|
||||
useWorker: false,
|
||||
showInvisibles: false,
|
||||
}}/>
|
||||
<Button color={"success"} variant={"contained"} onClick={onSubmitQuery} >Query</Button>
|
||||
</div>
|
||||
<TabContext style={{width: "100%", height: "100%", borderBottom: "1px solid grey"}} 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) => (
|
||||
<>
|
||||
{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"}}>
|
||||
{"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>
|
||||
</>
|
||||
))}
|
||||
</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 style={{overflow: "auto"}}>
|
||||
<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,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';
|
||||
@@ -89,7 +89,7 @@ export const getIconName = (iconName) => {
|
||||
case "camera":
|
||||
return faCamera;
|
||||
default:
|
||||
return faFileAlt;
|
||||
return iconName;
|
||||
}
|
||||
}
|
||||
export const getIconColor = (theme, color) => {
|
||||
@@ -360,9 +360,9 @@ const ResponseDisplayTableActionCellButton = ({cellData, callback_id}) => {
|
||||
<React.Fragment>
|
||||
<MythicStyledTooltip title={cellData?.button?.hoverText || "Submit Task"}>
|
||||
<Button size="small" onClick={() => setOpenTaskingButton(true)} disabled={cellData?.button?.disabled || false} color="warning"
|
||||
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.disabled ? "unset" : getIconColor(theme, cellData?.button?.startIconColor || "")}}/> : null}
|
||||
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.disabled ? "unset" : getIconColor(theme, cellData?.button?.startIconColor || "")}}/> : undefined}
|
||||
style={{...actionCellButtonStyle}}
|
||||
>{cellData?.button?.name ? cellData?.button?.name : cellData?.button?.startIcon ? null : "Submit Task"}</Button>
|
||||
>{cellData?.button?.name ? cellData?.button?.name : cellData?.button?.startIcon ? undefined : "Submit Task"}</Button>
|
||||
</MythicStyledTooltip>
|
||||
{openTaskingButton &&
|
||||
<TaskFromUIButton ui_feature={cellData?.button?.ui_feature || " "}
|
||||
@@ -408,10 +408,10 @@ const ResponseDisplayTableActionCellButton = ({cellData, callback_id}) => {
|
||||
table={taskingData?.value || {}} callback_id={callback_id} onClose={finishedViewingData} />}
|
||||
/>
|
||||
}
|
||||
<Button size="small" variant={"contained"} ref={dropdownAnchorRef}
|
||||
<Button size="small" ref={dropdownAnchorRef}
|
||||
onClick={() => setOpenDropdownButton(true)} disabled={cellData?.button?.disabled || false}
|
||||
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.disabled ? "unset" : getIconColor(theme, cellData?.button?.startIconColor || "")}}/> : null}
|
||||
style={{...actionCellButtonStyle}}
|
||||
style={{...actionCellButtonStyle, color: theme.palette.text.primary}}
|
||||
>{cellData?.button?.name || " "} <ArrowDropDownIcon />
|
||||
</Button>
|
||||
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
|
||||
@@ -585,9 +585,9 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
});
|
||||
}, [sortedData, table?.headers, callback_id]
|
||||
);
|
||||
const onSubmitFilterOptions = (newFilterOptions) => {
|
||||
setFilterOptions(newFilterOptions);
|
||||
}
|
||||
const onSubmitFilterOptions = (value) => {
|
||||
setFilterOptions({...filterOptions, [selectedColumn.key]: value });
|
||||
}
|
||||
|
||||
const filterOutButtonsFromRowData = (data) => {
|
||||
let rowData = {};
|
||||
@@ -666,7 +666,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
];
|
||||
const contextMenuOptions = [
|
||||
{
|
||||
name: 'Filter Column',
|
||||
name: 'Filter Column', type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -682,7 +682,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Copy Table as JSON",
|
||||
name: "Copy Table as JSON", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -691,7 +691,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Copy Table as CSV",
|
||||
name: "Copy Table as CSV", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -722,7 +722,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Copy Table as TSV",
|
||||
name: "Copy Table as TSV", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -753,7 +753,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Copy Table as PrettyPrint Text",
|
||||
name: "Copy Table as PrettyPrint Text", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -831,11 +831,16 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={<TableFilterDialog
|
||||
selectedColumn={selectedColumn}
|
||||
filterOptions={filterOptions}
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
onClose={()=>{setOpenContextMenu(false);}} />}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={filterOptions[selectedColumn.key]}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ import {operatorSettingDefaults} from "../../../cache";
|
||||
import {TaskFromUIButton} from "./TaskFromUIButton";
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {faSkullCrossbones} from '@fortawesome/free-solid-svg-icons';
|
||||
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
||||
// Icons for console-view display
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import PersonOutlineIcon from '@mui/icons-material/PersonOutline';
|
||||
@@ -253,6 +254,7 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh
|
||||
const initialShowHostnameValue = GetMythicSetting({setting_name: "showHostname", default_value: operatorSettingDefaults.showHostname});
|
||||
const initialShowCallbackGroupsValue = GetMythicSetting({setting_name: "showCallbackGroups", default_value: operatorSettingDefaults.showCallbackGroups});
|
||||
const initialTaskTimestampDisplayField = GetMythicSetting({setting_name: "taskTimestampDisplayField", default_value: operatorSettingDefaults.taskTimestampDisplayField});
|
||||
const initialShowOPSECBypassUsername = GetMythicSetting({setting_name: "showOPSECBypassUsername", default_value: operatorSettingDefaults.showOPSECBypassUsername});
|
||||
const displayTimestamp = task[initialTaskTimestampDisplayField] ? task[initialTaskTimestampDisplayField] : task.timestamp;
|
||||
const [openKillTaskButton, setOpenKillTaskButton] = React.useState({open: false});
|
||||
const toggleDisplayComment = (evt) => {
|
||||
@@ -288,46 +290,46 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh
|
||||
<Typography className={classes.taskAndTimeDisplay} onClick={preventPropagation}>
|
||||
[{toLocalTime(displayTimestamp, me?.user?.view_utc_time || false)}]
|
||||
{" / "}
|
||||
<span style={{}}>
|
||||
{task.has_intercepted_response &&
|
||||
<>
|
||||
<MythicStyledTooltip
|
||||
title={"This task has responses that have been intercepted and changed due to a workflow container"}>
|
||||
<IconButton style={{padding: 0}} color={"secondary"}>
|
||||
<CropRotateTwoToneIcon fontSize={"small"}/>
|
||||
</IconButton>
|
||||
|
||||
</MythicStyledTooltip>
|
||||
{"/ "}
|
||||
</>
|
||||
}
|
||||
{task?.eventstepinstance !== null &&
|
||||
<>
|
||||
<MythicStyledTooltip title={"Task created via Eventing, click to view entire event flow in separate page"} >
|
||||
<IconButton component={Link} href={'/new/eventing?eventgroup=' +
|
||||
task?.eventstepinstance?.eventgroupinstance?.eventgroup?.id +
|
||||
"&eventgroupinstance=" + task?.eventstepinstance?.eventgroupinstance?.id
|
||||
} target={"_blank"} style={{padding: 0}}
|
||||
color="inherit" disableFocusRipple={true}
|
||||
{task.has_intercepted_response &&
|
||||
<span style={{display:"inline-flex", whiteSpace: "pre-wrap"}}>
|
||||
<MythicStyledTooltip
|
||||
title={"This task has responses that have been intercepted and changed due to a workflow container"}>
|
||||
<IconButton color={"inherit"} style={{padding: 0}} disableFocusRipple={true}
|
||||
disableRipple={true}>
|
||||
<PlayCircleFilledTwoToneIcon />
|
||||
<CropRotateTwoToneIcon style={{height: "15px", cursor: "default"}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
|
||||
{"/ "}
|
||||
</>
|
||||
}
|
||||
<MythicStyledTooltip title={"View Task in separate page"} >
|
||||
<Link style={{wordBreak: "break-all", color: theme.taskPromptTextColor,}} underline={"always"} target={"_blank"}
|
||||
href={"/new/task/" + task.display_id}>T-{task.display_id}</Link>
|
||||
</MythicStyledTooltip>
|
||||
{" / "}
|
||||
</span>
|
||||
}
|
||||
{task?.eventstepinstance !== null &&
|
||||
<span style={{display:"inline-flex", whiteSpace: "pre-wrap"}}>
|
||||
<MythicStyledTooltip title={"Task created via Eventing, click to view entire event flow in separate page"} >
|
||||
<IconButton component={Link} href={'/new/eventing?eventgroup=' +
|
||||
task?.eventstepinstance?.eventgroupinstance?.eventgroup?.id +
|
||||
"&eventgroupinstance=" + task?.eventstepinstance?.eventgroupinstance?.id
|
||||
} target={"_blank"} style={{padding: 0, }}
|
||||
color="inherit" disableFocusRipple={true}
|
||||
disableRipple={true}>
|
||||
<PlayCircleFilledTwoToneIcon fontSize={"small"} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
|
||||
{" / "}
|
||||
</span>
|
||||
}
|
||||
<span style={{display:"inline-flex", whiteSpace: "pre-wrap"}}>
|
||||
<MythicStyledTooltip title={"View Task in separate page"} >
|
||||
<Link style={{wordBreak: "break-all", color: theme.taskPromptTextColor,}} underline={"always"} target={"_blank"}
|
||||
href={"/new/task/" + task.display_id}>T-{task.display_id}</Link>
|
||||
</MythicStyledTooltip>
|
||||
{!task.completed && task.status_timestamp_processing &&
|
||||
<>
|
||||
<MythicStyledTooltip title={"Task the agent to kill this task"} >
|
||||
<FontAwesomeIcon size={"sm"} icon={faSkullCrossbones} onClick={(e) => onClickKillIcon(e, true)}
|
||||
style={{cursor: "pointer", height: "15px", marginLeft: "5px"}} />
|
||||
</MythicStyledTooltip>
|
||||
</>
|
||||
|
||||
<MythicStyledTooltip title={"Task the agent to kill this task"} >
|
||||
<FontAwesomeIcon size={"sm"} icon={faSkullCrossbones} onClick={(e) => onClickKillIcon(e, true)}
|
||||
style={{cursor: "pointer", height: "15px", marginLeft: "5px"}} />
|
||||
</MythicStyledTooltip>
|
||||
|
||||
}
|
||||
</span>
|
||||
{" / "}
|
||||
@@ -343,16 +345,30 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh
|
||||
{initialShowCallbackGroupsValue ? `/ ${task.callback.mythictree_groups.join(', ')} ` : ''}
|
||||
{" / "}
|
||||
{task?.command?.payloadtype?.name}
|
||||
{" / "}
|
||||
{initialShowOPSECBypassUsername && <>
|
||||
{(task?.opsec_pre_bypass_user?.username || task?.opsec_post_bypass_user?.username) && <span
|
||||
style={{display:"inline-flex", whiteSpace: "pre-wrap"}}>
|
||||
{" /"}
|
||||
<MythicStyledTooltip title={"The specified usernames approved OPSEC bypasses for this task"}
|
||||
tooltipStyle={{padding: 0}}>
|
||||
<LockOpenIcon color={"inherit"} style={{height: "15px"}}/>
|
||||
</MythicStyledTooltip>
|
||||
{task?.opsec_pre_bypass_user?.username}
|
||||
{task?.opsec_pre_bypass_user?.username && task?.opsec_post_bypass_user?.username && ", "}
|
||||
{task?.opsec_post_bypass_user?.username}
|
||||
</span>}
|
||||
</>}
|
||||
{" "}
|
||||
<TaskStatusDisplay task={task} theme={theme}/>
|
||||
{task.comment.length > 0 ? (
|
||||
<span className={classes.column}>
|
||||
{" / "}
|
||||
<IconButton size="small" style={{padding: "0"}}
|
||||
onClick={toggleDisplayComment}><ChatOutlinedIcon fontSize={"small"}/></IconButton>
|
||||
</span>
|
||||
) : null}
|
||||
{task.comment}
|
||||
</Typography>
|
||||
</Typography>
|
||||
<TaskTagDisplay task={task}/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -425,7 +441,7 @@ const TaskRow = ({task, filterOptions, me, newlyIssuedTasks, indentLevel, collap
|
||||
"parameterString": onlyParameters,
|
||||
"hideErrors": hideErrors
|
||||
}); */
|
||||
if(hideBrowserTasking && task.tasking_location.includes("browser")){
|
||||
if(hideBrowserTasking && task.tasking_location.includes("_browser")){
|
||||
setShouldDisplay(false);
|
||||
return;
|
||||
}
|
||||
@@ -508,6 +524,10 @@ const TaskRow = ({task, filterOptions, me, newlyIssuedTasks, indentLevel, collap
|
||||
return;
|
||||
}
|
||||
setDisplayChildren(!displayChildren);
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
}, [displayChildren]);
|
||||
useEffect( () => {
|
||||
if(collapseAllRequest > 0){
|
||||
|
||||
@@ -36,6 +36,11 @@ import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen';
|
||||
import CodeOffIcon from '@mui/icons-material/CodeOff';
|
||||
import SettingsTwoToneIcon from '@mui/icons-material/SettingsTwoTone';
|
||||
import {b64DecodeUnicode} from './ResponseDisplay';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {EventTriggerContextSelectDialog} from "../Eventing/EventTriggerContextSelect";
|
||||
import PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwoTone';
|
||||
|
||||
const ReissueTaskMutationGQL = gql`
|
||||
mutation reissueTaskMutation($task_id: Int!){
|
||||
@@ -131,7 +136,7 @@ export const TaskDisplayContainerFlat = ({task, me}) => {
|
||||
}, [searchOutput]);
|
||||
|
||||
return (
|
||||
<div style={{ height: "100%", position: "relative", display: "flex", flexDirection: "column", overflowY: "auto", }}>
|
||||
<div style={{ height: "100%", position: "relative", display: "flex", overflowY: "auto", justifyContent: "flex-end" }}>
|
||||
<ResponseDisplay
|
||||
key={task.id}
|
||||
task={task}
|
||||
@@ -143,7 +148,7 @@ export const TaskDisplayContainerFlat = ({task, me}) => {
|
||||
expand={true}
|
||||
responseRef={responseRef}
|
||||
/>
|
||||
<SpeedDialDisplayGeneric toggleViewBrowserScript={toggleViewBrowserScript}
|
||||
<SideDisplayGeneric toggleViewBrowserScript={toggleViewBrowserScript}
|
||||
toggleSelectAllOutput={toggleSelectAllOutput}
|
||||
toggleOpenSearch={toggleOpenSearch}
|
||||
taskData={task}
|
||||
@@ -179,11 +184,283 @@ export const TaskDisplayContainerConsole = ({task, me}) => {
|
||||
);
|
||||
}
|
||||
|
||||
const SideDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput,
|
||||
toggleOpenSearch, taskData, viewAllOutput, me,
|
||||
responseRef, viewBrowserScript}) => {
|
||||
const theme = useTheme();
|
||||
const [task, setTask] = React.useState(taskData || {});
|
||||
const eventingDataRef = React.useRef({name: "", value: 0});
|
||||
const [openEventingDialog, setOpenEventingDialog] = React.useState(false);
|
||||
const [openTaskTagDialog, setOpenTaskTagDialog] = React.useState(false);
|
||||
const [openCommentDialog, setOpenCommentDialog] = React.useState(false);
|
||||
const [openParametersDialog, setOpenParametersDialog] = React.useState(false);
|
||||
const [openTokenDialog, setOpenTokenDialog] = React.useState(false);
|
||||
const [openStdoutStderrDialog, setOpenStdoutStderrDialog] = React.useState(false);
|
||||
const [openOpsecDialog, setOpenOpsecDialog] = React.useState({open: false, view: "pre"});
|
||||
const [downloadResponses] = useLazyQuery(getAllResponsesLazyQuery, {
|
||||
fetchPolicy: "network-only",
|
||||
onCompleted: (data) => {
|
||||
const output = data.response.reduce( (prev, cur) => {
|
||||
return prev + b64DecodeUnicode(cur.response);
|
||||
}, b64DecodeUnicode(""));
|
||||
downloadFileFromMemory(output, "task_" + task.id + ".txt");
|
||||
},
|
||||
onError: (data) => {
|
||||
|
||||
}
|
||||
});
|
||||
React.useEffect( () => {
|
||||
setTask(taskData);
|
||||
}, [taskData.id, taskData.token, taskData.original_params, taskData.opsec_pre_blocked, taskData.opsec_pre_bypassed, taskData.opsec_post_blocked, taskData.opsec_post_bypassed])
|
||||
const onDownloadResponses = () => {
|
||||
downloadResponses({variables: {task_id: task.id}});
|
||||
};
|
||||
const copyToClipboard = () => {
|
||||
let command = task?.command?.cmd || task.command_name;
|
||||
let result = copyStringToClipboard(command + " " + task.original_params);
|
||||
if(result){
|
||||
snackActions.success("Copied text!");
|
||||
}else{
|
||||
snackActions.error("Failed to copy text");
|
||||
}
|
||||
};
|
||||
const [reissueTask] = useMutation(ReissueTaskMutationGQL, {
|
||||
onCompleted: data => {
|
||||
if(data.reissue_task.status === "success"){
|
||||
snackActions.success("Successfully re-issued task to Mythic");
|
||||
}else{
|
||||
snackActions.error("Failed to re-issue task to Mythic: " + data.reissue_task.error);
|
||||
}
|
||||
},
|
||||
onError: data => {
|
||||
console.log(data);
|
||||
snackActions.error("Failed to re-issue task: " + data);
|
||||
}
|
||||
});
|
||||
const [reissueTaskHandler] = useMutation(ReissueTaskHandlerMutationGQL, {
|
||||
onCompleted: data => {
|
||||
if(data.reissue_task_handler.status === "success"){
|
||||
snackActions.success("Successfully resubmitted task for handling");
|
||||
}else{
|
||||
snackActions.warning("Failed to resubmit task for handling: " + data.reissue_task_handler.error);
|
||||
}
|
||||
|
||||
},
|
||||
onError: data => {
|
||||
console.log(data);
|
||||
snackActions.error("Error resubmitting task for handling: " + data);
|
||||
}
|
||||
});
|
||||
const onDownloadImageClickPng = () => {
|
||||
// we calculate a transform for the nodes so that all nodes are visible
|
||||
// we then overwrite the transform of the `.react-flow__viewport` element
|
||||
// with the style option of the html-to-image library
|
||||
snackActions.info("Saving image to png...");
|
||||
(async () => {
|
||||
const canvas = await html2canvas(responseRef.current);
|
||||
const image = canvas.toDataURL("image/png", 1.0);
|
||||
const fakeLink = window.document.createElement("a");
|
||||
fakeLink.style = "display:none;";
|
||||
fakeLink.download = "task_output.png";
|
||||
|
||||
fakeLink.href = image;
|
||||
|
||||
document.body.appendChild(fakeLink);
|
||||
fakeLink.click();
|
||||
document.body.removeChild(fakeLink);
|
||||
|
||||
fakeLink.remove();
|
||||
|
||||
})();
|
||||
};
|
||||
const onReissueTask = () => {
|
||||
reissueTask({variables: {task_id: task.id}});
|
||||
}
|
||||
const onReissueTaskHandler = () => {
|
||||
reissueTaskHandler({variables: {task_id: task.id}});
|
||||
}
|
||||
const onTriggerEventing = () => {
|
||||
eventingDataRef.current = {
|
||||
name: "task_id",
|
||||
value: task.id
|
||||
};
|
||||
setOpenEventingDialog(true);
|
||||
}
|
||||
return (
|
||||
<div style={{height: "100%"}}>
|
||||
{openTaskTagDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="lg" open={openTaskTagDialog}
|
||||
onClose={()=>{setOpenTaskTagDialog(false);}}
|
||||
innerDialog={<ViewEditTagsDialog me={me} target_object={"task_id"} target_object_id={task.id} onClose={()=>{setOpenTaskTagDialog(false);}} />}
|
||||
/>) : null}
|
||||
{openCommentDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="lg" open={openCommentDialog}
|
||||
onClose={()=>{setOpenCommentDialog(false);}}
|
||||
innerDialog={<TaskCommentDialog task_id={task.id} onClose={()=>{setOpenCommentDialog(false);}} />}
|
||||
/>) : null
|
||||
}
|
||||
{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}
|
||||
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}
|
||||
onClose={()=>{setOpenOpsecDialog({...openOpsecDialog, open: false});}}
|
||||
innerDialog={<TaskOpsecDialog task_id={task.id} view={openOpsecDialog.view} onClose={()=>{setOpenOpsecDialog({...openOpsecDialog, open: false});}} />}
|
||||
/>) : null
|
||||
}
|
||||
|
||||
{openStdoutStderrDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="lg" open={openStdoutStderrDialog}
|
||||
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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
<Paper elevation={5} style={{width: "30px", display: "flex", flexDirection: "column", alignItems: "center", overflow: "hidden",
|
||||
backgroundColor: "transparent", height: "100%"}}>
|
||||
<MythicStyledTooltip title={"Toggle BrowserScript"} >
|
||||
<IconButton onClick={toggleViewBrowserScript} style={{paddingBottom: "3px"}}>
|
||||
{viewBrowserScript ? <CodeOffIcon color={"error"} /> : <CodeIcon color={"success"}/>}
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={viewAllOutput ? "View Paginated Output" : "View All Output"}>
|
||||
<IconButton onClick={toggleSelectAllOutput} style={{paddingBottom: "3px"}}>
|
||||
{viewAllOutput ? <CloseFullscreenIcon color={"error"} /> : <FontAwesomeIcon style={{color: theme.palette.success.main}} icon={faExpandArrowsAlt} size="sm" />}
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Search Output"}>
|
||||
<IconButton onClick={toggleOpenSearch} style={{paddingBottom: "3px"}}>
|
||||
<SearchIcon color={"info"} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Download output"}>
|
||||
<IconButton onClick={onDownloadResponses} style={{paddingBottom: "3px"}}>
|
||||
<GetAppIcon color={"success"}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Download screenshot of output"}>
|
||||
<IconButton onClick={onDownloadImageClickPng} style={{paddingBottom: "3px"}}>
|
||||
<InsertPhotoIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Edit Tags"}>
|
||||
<IconButton onClick={()=>{setOpenTaskTagDialog(true)}} style={{paddingBottom: "3px"}}>
|
||||
<LocalOfferOutlinedIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Open Task in New Window"}>
|
||||
<IconButton onClick={()=> {window.open('/new/task/' + task.display_id, "_blank")}} style={{paddingBottom: "3px"}}>
|
||||
<FontAwesomeIcon icon={faExternalLinkAlt} size="sm" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Copy original params to clipboard"}>
|
||||
<IconButton onClick={copyToClipboard} style={{paddingBottom: "3px"}}>
|
||||
<FileCopyOutlinedIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Edit Comment"}>
|
||||
<IconButton onClick={()=>{setOpenCommentDialog(true)}} style={{paddingBottom: "3px"}}>
|
||||
<RateReviewOutlinedIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"View All Parameters And Timestamps"}>
|
||||
<IconButton onClick={()=>{setOpenParametersDialog(true);}} style={{paddingBottom: "3px"}}>
|
||||
<KeyboardIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"View Stdout/Stderr of Task"}>
|
||||
<IconButton onClick={()=>{setOpenStdoutStderrDialog(true);}} style={{paddingBottom: "3px"}}>
|
||||
<FontAwesomeIcon style={{color: theme.palette.error.main}} icon={faExclamationTriangle} size="sm" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Trigger Eventing Based on Task"}>
|
||||
<IconButton onClick={()=>{onTriggerEventing();}} style={{paddingBottom: "3px"}}>
|
||||
<PlayCircleFilledTwoToneIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
{task.opsec_pre_blocked === null ? null : ( task.opsec_pre_bypassed === false ? (
|
||||
<MythicStyledTooltip title={"Submit OPSEC PreCheck Bypass Request"}>
|
||||
<IconButton onClick={()=>{setOpenOpsecDialog({open: true, view: "pre"})}} style={{paddingBottom: "3px"}}>
|
||||
<LockIcon style={{color: theme.palette.error.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
): (
|
||||
<MythicStyledTooltip title={"View OPSEC PreCheck Data"}>
|
||||
<IconButton onClick={()=>{setOpenOpsecDialog({open: true, view: "pre"})}} style={{paddingBottom: "3px"}}>
|
||||
<LockOpenIcon style={{color: theme.palette.success.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
)
|
||||
)
|
||||
}
|
||||
{task.opsec_post_blocked === null ? null : ( task.opsec_post_bypassed === false ? (
|
||||
<MythicStyledTooltip title={"Submit OPSEC PostCheck Bypass Request"}>
|
||||
<IconButton onClick={()=>{setOpenOpsecDialog({open: true, view: "post"})}} style={{paddingBottom: "3px"}}>
|
||||
<LockIcon style={{color: theme.palette.error.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
): (
|
||||
<MythicStyledTooltip title={"View OPSEC PostCheck Data"}>
|
||||
<IconButton onClick={()=>{setOpenOpsecDialog({open: true, view: "post"})}} style={{paddingBottom: "3px"}}>
|
||||
<LockOpenIcon style={{color: theme.palette.success.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
)
|
||||
)
|
||||
}
|
||||
{task.token === null ? null : (
|
||||
<MythicStyledTooltip title={"View Token Information"}>
|
||||
<IconButton onClick={()=>{setOpenTokenDialog(true)}} style={{paddingBottom: "3px"}}>
|
||||
<ConfirmationNumberIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
)}
|
||||
{task.status.toLowerCase().includes("error: container") &&
|
||||
<MythicStyledTooltip title={"Resubmit Tasking"}>
|
||||
<IconButton onClick={onReissueTask} style={{paddingBottom: "3px"}}>
|
||||
<ReplayIcon style={{color: theme.palette.warning.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
{task.status.toLowerCase().includes("error: task") &&
|
||||
<MythicStyledTooltip title={"Resubmit Task Handler"}>
|
||||
<IconButton onClick={onReissueTaskHandler} style={{paddingBottom: "3px"}}>
|
||||
<ReplayIcon style={{color: theme.palette.warning.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
</Paper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput,
|
||||
toggleOpenSearch, taskData, viewAllOutput, me,
|
||||
responseRef, style, fabStyle, viewBrowserScript}) => {
|
||||
const theme = useTheme();
|
||||
const [task, setTask] = React.useState(taskData || {});
|
||||
const eventingDataRef = React.useRef({name: "", value: 0});
|
||||
const [openEventingDialog, setOpenEventingDialog] = React.useState(false);
|
||||
const [openSpeedDial, setOpenSpeedDial] = React.useState(false);
|
||||
const [openTaskTagDialog, setOpenTaskTagDialog] = React.useState(false);
|
||||
const [openCommentDialog, setOpenCommentDialog] = React.useState(false);
|
||||
@@ -275,6 +552,13 @@ 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"}}/>
|
||||
@@ -314,6 +598,19 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput
|
||||
innerDialog={<TaskViewStdoutStderrDialog task_id={task.id} onClose={()=>{setOpenStdoutStderrDialog(false);}} />}
|
||||
/>) : null
|
||||
}
|
||||
{openEventingDialog &&
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
maxWidth="xl"
|
||||
open={openEventingDialog}
|
||||
onClose={() => {setOpenEventingDialog(false);}}
|
||||
innerDialog={
|
||||
<EventTriggerContextSelectDialog onClose={() => {setOpenEventingDialog(false);}}
|
||||
triggerContext={eventingDataRef.current}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
<SpeedDial
|
||||
ariaLabel="Task Speeddial"
|
||||
icon={<SettingsTwoToneIcon fontSize={"large"} />}
|
||||
@@ -400,6 +697,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}}/>}
|
||||
|
||||
@@ -256,6 +256,7 @@ query getCommandQuery($id: Int!){
|
||||
parameter_group_name
|
||||
display_name
|
||||
cli_name
|
||||
verifier_regex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -741,6 +741,7 @@ export function TaskParametersDialogRow(props){
|
||||
</FormControl>
|
||||
OR
|
||||
<MythicTextField required={props.required} placeholder={"Custom Value"} value={chooseOneCustomValue} multiline={true} maxRows={5}
|
||||
validate={testParameterValues} errorText={"Must match: " + props.verifier_regex}
|
||||
onChange={onChangeTextChooseOneCustom} display="inline-block" onEnter={props.onSubmit} autoFocus={props.autoFocus}
|
||||
name={props.name} marginTop={"5px"}
|
||||
/>
|
||||
@@ -1198,15 +1199,16 @@ export function TaskParametersDialogRow(props){
|
||||
return (
|
||||
<TableRow key={"buildparam" + props.id}>
|
||||
<MythicStyledTableCell >
|
||||
<Typography style={{fontWeight: "600"}} >
|
||||
<Typography style={{fontWeight: "600", wordBreak: "break-all"}} >
|
||||
{props.display_name}
|
||||
</Typography>
|
||||
<Typography variant={"body2"} style={{fontSize: theme.typography.pxToRem(15)}}>
|
||||
<Typography variant={"body2"} style={{
|
||||
fontSize: theme.typography.pxToRem(14),
|
||||
wordBreak: "break-all"
|
||||
}}>
|
||||
{props.description}
|
||||
</Typography>
|
||||
{props.required ? (
|
||||
<Typography component="div" color={"warning"}>Required</Typography>
|
||||
) : null }
|
||||
{props.required && <Typography component="div" color={"warning"}>Required</Typography>}
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
{getParameterObject()}
|
||||
|
||||
@@ -25,7 +25,6 @@ import { Backdrop } from '@mui/material';
|
||||
import {CircularProgress} from '@mui/material';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import { FormControlLabel } from '@mui/material';
|
||||
|
||||
export const getDynamicQueryBuildParameterParams = gql`
|
||||
mutation getDynamicBuildParamsMutation($payload_type: String!, $parameter_name: String!, $selected_os: String!){
|
||||
|
||||
@@ -730,6 +730,18 @@ export const sortByUiPositionThenName = (a, b) => {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
const HideConditionOperandEQ = "eq"
|
||||
const HideConditionOperandNotEQ = "neq"
|
||||
const HideConditionOperandIN = "in"
|
||||
const HideConditionOperandNotIN = "nin"
|
||||
const HideConditionOperandLessThan = "lt"
|
||||
const HideConditionOperandGreaterThan = "gt"
|
||||
const HideConditionOperandLessThanOrEqual = "lte"
|
||||
const HideConditionOperandGreaterThanOrEqual = "gte"
|
||||
const HideConditionOperationStartsWith = "sw"
|
||||
const HideConditionOperationEndsWith = "ew"
|
||||
const HideConditionOperationContains = "co"
|
||||
const HideConditionOperationNotContains = "nco"
|
||||
export const GetGroupedParameters = ({buildParameters, os, c2_name}) => {
|
||||
let groups = buildParameters?.reduce( (prev, cur) => {
|
||||
if(prev.includes(cur?.group_name)){return [...prev]}
|
||||
@@ -756,26 +768,83 @@ export const GetGroupedParameters = ({buildParameters, os, c2_name}) => {
|
||||
for(let l = 0; l < buildParameters.length; l++){
|
||||
if(buildParameters[l].name === buildParameters[i].hide_conditions[k].name){
|
||||
switch(buildParameters[i].hide_conditions[k].operand){
|
||||
case "eq":
|
||||
case HideConditionOperandEQ:
|
||||
if(String(buildParameters[i].hide_conditions[k].value) === String(buildParameters[l].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
break;
|
||||
case "neq":
|
||||
case HideConditionOperandNotEQ:
|
||||
if(String(buildParameters[i].hide_conditions[k].value) !== String(buildParameters[l].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
break;
|
||||
case "in":
|
||||
case HideConditionOperandIN:
|
||||
if(buildParameters[i].hide_conditions[k].choices.includes(buildParameters[l].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
break;
|
||||
case "nin":
|
||||
case HideConditionOperandNotIN:
|
||||
if(!buildParameters[i].hide_conditions[k].choices.includes(buildParameters[l].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
break;
|
||||
case HideConditionOperandLessThan:
|
||||
try{
|
||||
if(parseInt(buildParameters[l].value) < parseInt(buildParameters[i].hide_conditions[k].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
}catch(e){
|
||||
console.log("couldn't parse build parameter value as int", e);
|
||||
}
|
||||
|
||||
break;
|
||||
case HideConditionOperandLessThanOrEqual:
|
||||
try{
|
||||
if(parseInt(buildParameters[l].value) <= parseInt(buildParameters[i].hide_conditions[k].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
}catch(e){
|
||||
console.log("couldn't parse build parameter value as int", e);
|
||||
}
|
||||
break;
|
||||
case HideConditionOperandGreaterThan:
|
||||
try{
|
||||
if(parseInt(buildParameters[l].value) > parseInt(buildParameters[i].hide_conditions[k].value) ){
|
||||
should_hide = true;
|
||||
}
|
||||
}catch(e){
|
||||
console.log("couldn't parse build parameter value as int", e);
|
||||
}
|
||||
break;
|
||||
case HideConditionOperandGreaterThanOrEqual:
|
||||
try{
|
||||
if(parseInt(buildParameters[l].value) >= parseInt(buildParameters[i].hide_conditions[k].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
}catch(e){
|
||||
console.log("couldn't parse build parameter value as int", e);
|
||||
}
|
||||
break;
|
||||
case HideConditionOperationStartsWith:
|
||||
if(String(buildParameters[l].value).startsWith(String(buildParameters[i].hide_conditions[k].value))){
|
||||
should_hide = true;
|
||||
}
|
||||
break;
|
||||
case HideConditionOperationEndsWith:
|
||||
if(String(buildParameters[l].value).endsWith(String(buildParameters[i].hide_conditions[k].value))){
|
||||
should_hide = true;
|
||||
}
|
||||
break;
|
||||
case HideConditionOperationContains:
|
||||
if(buildParameters[l].value.includes(buildParameters[i].hide_conditions[k].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
break;
|
||||
case HideConditionOperationNotContains:
|
||||
if(!buildParameters[l].value.includes(buildParameters[i].hide_conditions[k].value)){
|
||||
should_hide = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -820,8 +889,6 @@ export const ConfigurationSummary = ({buildParameters, os, c2_name}) => {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
</div>
|
||||
))
|
||||
)
|
||||
|
||||
@@ -101,6 +101,7 @@ export function Step3SelectPayload(props){
|
||||
options.push({name: data.payloadtype_by_pk.wrap_these_payload_types[i].wrapped.name, ...data.payloadtype_by_pk.wrap_these_payload_types[i].wrapped.payloads[j] })
|
||||
}
|
||||
}
|
||||
options = options.sort((a,b) => new Date(a.creation_time) < new Date(b.creation_time) ? 1 : -1)
|
||||
setPayloadOptions(options);
|
||||
setOpenBackdrop(false);
|
||||
}else{
|
||||
|
||||
@@ -13,13 +13,12 @@ import Moment from 'react-moment';
|
||||
import moment from 'moment';
|
||||
import CastConnectedTwoToneIcon from '@mui/icons-material/CastConnectedTwoTone';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import {MythicDialog} from "../../MythicComponents/MythicDialog";
|
||||
import {MythicDialog, MythicModifyStringDialog} from "../../MythicComponents/MythicDialog";
|
||||
import OpenInNewTwoToneIcon from '@mui/icons-material/OpenInNewTwoTone';
|
||||
import IosShareIcon from '@mui/icons-material/IosShare';
|
||||
import {copyStringToClipboard} from "../../utilities/Clipboard";
|
||||
import {ipCompare} from "../Callbacks/CallbacksTable";
|
||||
import MythicResizableGrid from "../../MythicComponents/MythicResizableGrid";
|
||||
import {TableFilterDialog} from "../Callbacks/TableFilterDialog";
|
||||
import {CallbacksTableStringCell} from "../Callbacks/CallbacksTableRow";
|
||||
|
||||
const cancelEventGroupInstanceMutation = gql(`
|
||||
@@ -171,15 +170,25 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
|
||||
}
|
||||
|
||||
}, [eventgroups]);
|
||||
const columns = [
|
||||
{key: "id", type: "number", name: "ID", width: 60, enableHiding: false, disableSort: true, disableFilter: true},
|
||||
{key: "status", type: 'string', name: "Status", width: 150, disableSort: true, enableHiding: false},
|
||||
{key: "event_group", type: 'string', name: "Event Group", inMetadata: true, fillWidth: true, disableSort: true, enableHiding: false},
|
||||
{key: "trigger", type: 'string', name: "Trigger", fillWidth: true, disableSort: true, enableHiding: false},
|
||||
{key: "time", type: 'date', name: "Time", width: 300, disableSort: true,},
|
||||
{key: "operator", type: 'string', name: "Operator", inMetadata: true, fillWidth: true, disableSort: true,},
|
||||
{key: "cancel", type: 'string', name: "Action", width: 70, disableSort: true, disableFilter: true},
|
||||
];
|
||||
const columns = React.useMemo(
|
||||
() =>
|
||||
[
|
||||
{key: "id", type: "number", name: "ID", width: 60, enableHiding: false, disableSort: false, disableFilter: true},
|
||||
{key: "status", type: 'string', name: "Status", width: 150, disableSort: true, enableHiding: false},
|
||||
{key: "eventgroup", type: 'string', name: "Event Group", inMetadata: true, fillWidth: true, disableSort: false, enableHiding: false},
|
||||
{key: "trigger", type: 'string', name: "Trigger", fillWidth: true, disableSort: false, enableHiding: false},
|
||||
{key: "time", type: 'date', name: "Time", width: 300, disableSort: true,},
|
||||
{key: "operator", type: 'string', name: "Operator", inMetadata: true, fillWidth: true, disableSort: true,},
|
||||
{key: "cancel", type: 'string', name: "Action", width: 70, disableSort: true, disableFilter: true},
|
||||
]?.reduce( (prev, cur) => {
|
||||
if(filterOptions[cur.key] && String(filterOptions[cur.key]).length > 0){
|
||||
return [...prev, {...cur, filtered: true}];
|
||||
}else{
|
||||
return [...prev, {...cur}];
|
||||
}
|
||||
}, []) || []
|
||||
, [filterOptions]
|
||||
);
|
||||
const onClickHeader = (e, columnIndex) => {
|
||||
const column = columns[columnIndex];
|
||||
if(column.disableSort){
|
||||
@@ -219,7 +228,11 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
|
||||
if (sortData.sortType === 'number' || sortData.sortType === 'size' || sortData.sortType === 'date') {
|
||||
tempData.sort((a, b) => (parseInt(a[sortData.sortKey]) > parseInt(b[sortData.sortKey]) ? 1 : -1));
|
||||
} else if (sortData.sortType === 'string') {
|
||||
tempData.sort((a, b) => (a[sortData.sortKey].toLowerCase() > b[sortData.sortKey].toLowerCase() ? 1 : -1));
|
||||
if(sortData.sortKey === "eventgroup"){
|
||||
tempData.sort((a, b) => (a[sortData.sortKey].name.toLowerCase() > b[sortData.sortKey].name.toLowerCase() ? 1 : -1));
|
||||
} else {
|
||||
tempData.sort((a, b) => (a[sortData.sortKey].toLowerCase() > b[sortData.sortKey].toLowerCase() ? 1 : -1));
|
||||
}
|
||||
} else if (sortData.sortType === "ip") {
|
||||
tempData.sort((a, b) => (ipCompare(a[sortData.sortKey], b[sortData.sortKey])));
|
||||
} else if (sortData.sortType === "array") {
|
||||
@@ -364,11 +377,11 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
|
||||
const sortColumn = columns.findIndex((column) => column.key === sortData.sortKey);
|
||||
const [openContextMenu, setOpenContextMenu] = React.useState(false);
|
||||
const [selectedColumn, setSelectedColumn] = React.useState({});
|
||||
const onSubmitFilterOptions = (newFilterOptions) => {
|
||||
setFilterOptions(newFilterOptions);
|
||||
const onSubmitFilterOptions = (value) => {
|
||||
setFilterOptions({...filterOptions, [selectedColumn.key]: value });
|
||||
}
|
||||
const contextMenuOptions = [{
|
||||
name: 'Filter Column',
|
||||
name: 'Filter Column', type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -402,13 +415,18 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
|
||||
onRowContextMenuClick={onRowContextClick}
|
||||
/>
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xs" open={openContextMenu}
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={<TableFilterDialog
|
||||
selectedColumn={selectedColumn}
|
||||
filterOptions={filterOptions}
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
onClose={()=>{setOpenContextMenu(false);}} />}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={filterOptions[selectedColumn.key]}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
{openDeleteDialog &&
|
||||
|
||||
@@ -614,6 +614,7 @@ function EventStepRender({selectedEventGroup, useSuppliedData}) {
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
nodeTypes={nodeTypes}
|
||||
onNodeClick={onPaneClick}
|
||||
onPaneClick={onPaneClick}
|
||||
onNodeContextMenu={onNodeContextMenu}
|
||||
>
|
||||
@@ -622,7 +623,7 @@ function EventStepRender({selectedEventGroup, useSuppliedData}) {
|
||||
EventGroup: {selectedEventGroup.id}
|
||||
</Typography>
|
||||
}
|
||||
<Controls showInteractive={false}>
|
||||
<Controls showInteractive={false} style={{color: "black"}}>
|
||||
</Controls>
|
||||
</ReactFlow>
|
||||
{openContextMenu &&
|
||||
@@ -905,13 +906,14 @@ function EventStepInstanceRender({selectedEventGroupInstance}) {
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
nodeTypes={nodeTypes}
|
||||
onNodeClick={onPaneClick}
|
||||
onPaneClick={onPaneClick}
|
||||
onNodeContextMenu={onNodeContextMenu}
|
||||
>
|
||||
<Typography component={"h1"} style={{marginLeft: "10px"}}>
|
||||
Instance: {selectedEventGroupInstance}
|
||||
</Typography>
|
||||
<Controls showInteractive={false}>
|
||||
<Controls showInteractive={false} style={{color: "black"}}>
|
||||
</Controls>
|
||||
</ReactFlow>
|
||||
{openContextMenu &&
|
||||
|
||||
@@ -310,8 +310,10 @@ export function Eventing({me}){
|
||||
/>
|
||||
}
|
||||
<ListItem onClick={() => setSelectedEventGroup({id: 0})}
|
||||
className={selectedEventGroup.id === 0 ? "selectedCallback": ""}
|
||||
style={selectedEventGroup.id === 0 ?
|
||||
{paddingTop: 0, paddingBottom: 0, borderLeft: `5px solid ${theme.palette.info.main}`} :
|
||||
{paddingTop: 0, paddingBottom: 0, borderLeft: `5px solid ${theme.palette.info.main}`,
|
||||
borderRight: `5px solid ${theme.palette.info.main}`} :
|
||||
{paddingTop: 0, paddingBottom: 0}}>
|
||||
<ListItemText primary={"View All Instances"} />
|
||||
</ListItem>
|
||||
@@ -335,8 +337,11 @@ export function Eventing({me}){
|
||||
{eventgroups.map( (e, i) => (
|
||||
(showDeleted || !e.deleted) &&
|
||||
<ListItem key={e.id + e.name} onClick={() => setSelectedEventGroup(e)}
|
||||
className={selectedEventGroup.id === e.id ? "selectedCallback": ""}
|
||||
style={selectedEventGroup.id === e.id ?
|
||||
{paddingTop: 0, paddingBottom: 0, borderLeft: `5px solid ${theme.palette.info.main}`} :
|
||||
{paddingTop: 0, paddingBottom: 0,
|
||||
borderLeft: `5px solid ${theme.palette.info.main}`, borderRight: `5px solid ${theme.palette.info.main}`,
|
||||
} :
|
||||
{paddingTop: 0, paddingBottom: 0}}>
|
||||
{!e.active &&
|
||||
<NotificationsOffTwoToneIcon color={"warning"} />
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import React from 'react';
|
||||
import AttachFileIcon from '@mui/icons-material/AttachFile';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import {useMutation, gql} from '@apollo/client';
|
||||
import {snackActions} from '../../utilities/Snackbar';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import {faFolderOpen} from '@fortawesome/free-solid-svg-icons';
|
||||
import {MythicConfirmDialog} from '../../MythicComponents/MythicConfirmDialog';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import RestoreFromTrashOutlinedIcon from '@mui/icons-material/RestoreFromTrashOutlined';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import MythicTableCell from "../../MythicComponents/MythicTableCell";
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import {MythicDialog} from "../../MythicComponents/MythicDialog";
|
||||
import {C2ProfileListFilesDialog} from "./C2ProfileListFilesDialog";
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
|
||||
const toggleDeleteStatus = gql`
|
||||
mutation toggleCustomBrowserDeleteStatus($custombrowser_id: Int!, $deleted: Boolean!){
|
||||
update_custombrowser_by_pk(pk_columns: {id: $custombrowser_id}, _set: {deleted: $deleted}) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function CustomBrowserRow({service, showDeleted}) {
|
||||
const theme = useTheme();
|
||||
const [openListFilesDialog, setOpenListFilesDialog] = React.useState(false);
|
||||
const [openDelete, setOpenDeleteDialog] = React.useState(false);
|
||||
const [updateDeleted] = useMutation(toggleDeleteStatus, {
|
||||
onCompleted: data => {
|
||||
},
|
||||
onError: error => {
|
||||
if(service.deleted){
|
||||
snackActions.error("Failed to restore browser");
|
||||
} else {
|
||||
snackActions.error("Failed to mark browser as deleted");
|
||||
}
|
||||
}
|
||||
});
|
||||
const onAcceptDelete = () => {
|
||||
updateDeleted({variables: {custombrowser_id: service.id, deleted: !service.deleted}})
|
||||
setOpenDeleteDialog(false);
|
||||
}
|
||||
if(service.deleted && !showDeleted){
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
|
||||
<TableRow >
|
||||
<MythicTableCell>
|
||||
{service.deleted ? (
|
||||
<IconButton size="small" onClick={()=>{setOpenDeleteDialog(true);}} color="success" variant="contained"><RestoreFromTrashOutlinedIcon/></IconButton>
|
||||
) : (
|
||||
<IconButton size="small" onClick={()=>{setOpenDeleteDialog(true);}} color="error" variant="contained"><DeleteIcon/></IconButton>
|
||||
)}
|
||||
{openDelete &&
|
||||
<MythicConfirmDialog onClose={() => {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete}
|
||||
open={openDelete}
|
||||
acceptText={service.deleted ? "Restore" : "Remove"}
|
||||
acceptColor={service.deleted ? "success": "error"} />
|
||||
}
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<FontAwesomeIcon icon={faFolderOpen} style={{width: "60px", height: "60px"}} />
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
{service.name}
|
||||
<Typography variant="body2" component="p" color={service.container_running ? theme.palette.success.main : theme.palette.error.main} >
|
||||
<b>{service.container_running ? "Online" : "Offline"}</b>
|
||||
</Typography>
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
{service.type}
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<Typography variant="body1" component="p">
|
||||
<b>Author:</b> {service.author}
|
||||
</Typography>
|
||||
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
|
||||
<b>Description: </b>{service.description}
|
||||
</Typography>
|
||||
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
|
||||
<b>Version: </b>{service.semver}
|
||||
</Typography>
|
||||
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
|
||||
<b>Export Capabilities: </b>{service.export_function === "" ? "False" : "True"}
|
||||
</Typography>
|
||||
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<MythicStyledTooltip title={service.container_running ? "View Files" : "Unable to view files because container is offline"}>
|
||||
<IconButton
|
||||
disabled={!service.container_running}
|
||||
onClick={()=>{setOpenListFilesDialog(true);}}
|
||||
size="medium">
|
||||
<AttachFileIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</MythicTableCell>
|
||||
<MythicTableCell style={{padding: 0}}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow hover>
|
||||
<MythicTableCell>{"Custom Row Action"}</MythicTableCell>
|
||||
<MythicTableCell>{"Needed UI Feature"}</MythicTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{service.row_actions.map(c => (
|
||||
<TableRow key={c.name} hover>
|
||||
<MythicTableCell>{c.name}</MythicTableCell>
|
||||
<MythicTableCell>{c.ui_feature}</MythicTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow hover>
|
||||
<MythicTableCell>{"Display Table Column"}</MythicTableCell>
|
||||
<MythicTableCell>{"Metadata Key"}</MythicTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{service.columns.map(c => (
|
||||
<TableRow key={c.name} hover>
|
||||
<MythicTableCell>{c.name}</MythicTableCell>
|
||||
<MythicTableCell>{c.key}</MythicTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow hover>
|
||||
<MythicTableCell>{"Extra Table Task Parameter"}</MythicTableCell>
|
||||
<MythicTableCell>{"Parameter Name"}</MythicTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{service.extra_table_inputs.map(c => (
|
||||
<TableRow key={c.name} hover>
|
||||
<MythicTableCell>
|
||||
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
|
||||
<b>{c.display_name}: </b> {c.description}
|
||||
</Typography>
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>{c.name}</MythicTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</MythicTableCell>
|
||||
|
||||
{openListFilesDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openListFilesDialog}
|
||||
onClose={()=>{setOpenListFilesDialog(false);}}
|
||||
innerDialog={<C2ProfileListFilesDialog container_name={service.name} {...service} onClose={()=>{setOpenListFilesDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
</TableRow>
|
||||
|
||||
);
|
||||
}
|
||||
+103
-7
@@ -20,6 +20,7 @@ import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import { IconButton } from '@mui/material';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import {CustomBrowserRow} from "./CustomBrowserCard";
|
||||
|
||||
const SUB_Payload_Types = gql`
|
||||
subscription getPayloadTypesSubscription {
|
||||
@@ -80,6 +81,7 @@ subscription getTranslationContainersSubscription {
|
||||
deleted
|
||||
description
|
||||
author
|
||||
semver
|
||||
payloadtypes(order_by: {name: asc}) {
|
||||
name
|
||||
deleted
|
||||
@@ -98,6 +100,25 @@ subscription ConsumingContainer{
|
||||
container_running
|
||||
deleted
|
||||
subscriptions
|
||||
semver
|
||||
}
|
||||
}
|
||||
`;
|
||||
const SUB_Custom_Browsers = gql`
|
||||
subscription CustomBrowser{
|
||||
custombrowser(order_by: {name: asc}) {
|
||||
id
|
||||
name
|
||||
description
|
||||
type
|
||||
container_running
|
||||
deleted
|
||||
columns
|
||||
export_function
|
||||
row_actions
|
||||
extra_table_inputs
|
||||
author
|
||||
semver
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -115,7 +136,7 @@ function useCustomSubscription({customSubscription, dataKey}){
|
||||
return customData
|
||||
}
|
||||
|
||||
const tabTypes = ["Payload Types", "C2 Profiles", "Translators", "Command Augmentation", "3rd Party", "Webhooks", "Loggers", "Eventing", "Auth", ];
|
||||
const tabTypes = ["Payload Types", "C2 Profiles", "Translators", "Command Augmentation", "3rd Party", "Webhooks", "Loggers", "Eventing", "Auth", "Browsers"];
|
||||
|
||||
const filterDeleted = (c, showDeleted) => {
|
||||
if(showDeleted){
|
||||
@@ -144,12 +165,16 @@ export function PayloadTypesC2Profiles({me}){
|
||||
customSubscription: sub_consuming_services,
|
||||
dataKey: "consuming_container"
|
||||
});
|
||||
const customBrowsers = useCustomSubscription({
|
||||
customSubscription: SUB_Custom_Browsers,
|
||||
dataKey: "custombrowser"
|
||||
})
|
||||
const [showDeleted, setShowDeleted] = React.useState(false);
|
||||
React.useEffect( () => {
|
||||
let newData = [...payloadTypes, ...c2Profiles, ...translationContainers, ...consumingServices];
|
||||
let newData = [...payloadTypes, ...c2Profiles, ...translationContainers, ...consumingServices, ...customBrowsers];
|
||||
newData.sort( (a, b) => a.name.toLowerCase() < b.name.toLowerCase() ? -1 : a.name.toLowerCase() > b.name.toLowerCase() ? 1 : 0);
|
||||
setAllData(newData);
|
||||
}, [payloadTypes, c2Profiles, translationContainers, consumingServices])
|
||||
}, [payloadTypes, c2Profiles, translationContainers, consumingServices, customBrowsers])
|
||||
const handleChange = (event, newValue) => {
|
||||
setValue(newValue);
|
||||
};
|
||||
@@ -182,6 +207,9 @@ export function PayloadTypesC2Profiles({me}){
|
||||
case 8:
|
||||
return <ContainersTabConsumingServicesPanel key={"Auth"} type={"Auth"} index={value} value={value} showDeleted={showDeleted}
|
||||
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "consuming_container" && c.type === "auth")} />
|
||||
case 9:
|
||||
return <ContainersTabCustomBrowsersPanel key={"Browsers"} type={"Browsers"} index={value} value={value} showDeleted={showDeleted}
|
||||
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "custombrowser")} />
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -243,6 +271,10 @@ export function PayloadTypesC2Profiles({me}){
|
||||
case "Auth":
|
||||
return <ContainersTabAuthLabel key={"auth"}
|
||||
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "consuming_container" && c.type === "auth")}/>;
|
||||
case "Browsers":
|
||||
return <ContainersTabBrowsersLabel key={"browsers"}
|
||||
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "custombrowser")}/>;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -270,21 +302,21 @@ const PayloadTypesC2ProfilesTableRow = ({service, showDeleted}) => {
|
||||
|
||||
const ContainersTabPayloadTypesLabel = (props) => {
|
||||
return (
|
||||
<MythicSearchTabLabel label={"Payload Types" + (props.containers.length > 0 ? " (" + props.containers.length + ") " : "")} iconComponent={
|
||||
<MythicSearchTabLabel label={"Agents" + (props.containers.length > 0 ? " (" + props.containers.length + ") " : "")} iconComponent={
|
||||
<></>
|
||||
} {...props}/>
|
||||
)
|
||||
}
|
||||
const ContainersTab3rdPartyLabel = (props) => {
|
||||
return (
|
||||
<MythicSearchTabLabel label={"3rd Party Service" + (props.containers.length > 0 ? " (" + props.containers.length + ") " : "")} iconComponent={
|
||||
<MythicSearchTabLabel label={"External" + (props.containers.length > 0 ? " (" + props.containers.length + ") " : "")} iconComponent={
|
||||
<></>
|
||||
} {...props}/>
|
||||
)
|
||||
}
|
||||
const ContainersTabC2ProfilesLabel = (props) => {
|
||||
return (
|
||||
<MythicSearchTabLabel label={"C2 Profiles" + (props.containers.length > 0 ? " (" + props.containers.length + ") " : "")} iconComponent={
|
||||
<MythicSearchTabLabel label={"C2" + (props.containers.length > 0 ? " (" + props.containers.length + ") " : "")} iconComponent={
|
||||
<></>
|
||||
} {...props}/>
|
||||
)
|
||||
@@ -326,7 +358,14 @@ const ContainersTabAuthLabel = (props) => {
|
||||
}
|
||||
const ContainersTabCommandAugmentLabel = (props) => {
|
||||
return (
|
||||
<MythicSearchTabLabel label={"Command Augmentation" + (props.containers.length > 0 ? " (" + props.containers.length + ") " : "")} iconComponent={
|
||||
<MythicSearchTabLabel label={"Command Augments" + (props.containers.length > 0 ? " (" + props.containers.length + ") " : "")} iconComponent={
|
||||
<></>
|
||||
} {...props}/>
|
||||
)
|
||||
}
|
||||
const ContainersTabBrowsersLabel = (props) => {
|
||||
return (
|
||||
<MythicSearchTabLabel label={"Browsers" + (props.containers.length > 0 ? " (" + props.containers.length + ") " : "")} iconComponent={
|
||||
<></>
|
||||
} {...props}/>
|
||||
)
|
||||
@@ -361,6 +400,9 @@ Command Augmentation container commands get "injected" into the callbacks of sup
|
||||
These are containers that generate "callbacks" and allow you to interact with a 3rd Party Service over an API from Mythic's standard tasking workflow.
|
||||
A few examples are github.com/MythicAgents/bloodhound and github.com/MythicAgents/ghostwriter.`;
|
||||
break;
|
||||
case "Browsers":
|
||||
message = `Custom Browsers allow you to collect and display data in a familiar and unified "browser" format.`;
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
@@ -473,3 +515,57 @@ You can extend this auth capability to support your own LDAP, SSO, or otherwise
|
||||
</MythicTabPanel>
|
||||
)
|
||||
}
|
||||
const ContainersTabCustomBrowsersPanel = (props) => {
|
||||
const getEmptyServiceListMessage = () => {
|
||||
let message = "";
|
||||
switch(props.type){
|
||||
case "Browsers":
|
||||
message = `Custom Browsers allow you to collect and display data in a familiar and unified "browser" format.`;
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<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",
|
||||
whiteSpace: "pre-wrap"
|
||||
}}>
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<MythicTabPanel {...props} >
|
||||
<div style={{display: "flex", flexGrow: 1, overflowY: "auto"}}>
|
||||
<TableContainer>
|
||||
<Table stickyHeader size="small" style={{"maxWidth": "100%", "overflow": "scroll"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicTableCell style={{width: "4rem"}}>Delete</MythicTableCell>
|
||||
<MythicTableCell style={{width: "90px"}}></MythicTableCell>
|
||||
<MythicTableCell>Service</MythicTableCell>
|
||||
<MythicTableCell style={{width: "4rem"}}>Type</MythicTableCell>
|
||||
<MythicTableCell>Metadata</MythicTableCell>
|
||||
<MythicTableCell>Manage</MythicTableCell>
|
||||
<MythicTableCell></MythicTableCell>
|
||||
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{props.containers.map((service, index) => (
|
||||
<CustomBrowserRow key={"service" + index} service={service} showDeleted={props.showDeleted}/>
|
||||
))}
|
||||
{props.containers.length === 0 && getEmptyServiceListMessage()}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</MythicTabPanel>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,17 +17,6 @@ import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import {MythicDialog} from "../../MythicComponents/MythicDialog";
|
||||
import {C2ProfileListFilesDialog} from "./C2ProfileListFilesDialog";
|
||||
|
||||
const PREFIX = 'TranslationContainerCard';
|
||||
|
||||
const classes = {
|
||||
root: `${PREFIX}-root`,
|
||||
expand: `${PREFIX}-expand`,
|
||||
expandOpen: `${PREFIX}-expandOpen`,
|
||||
running: `${PREFIX}-running`,
|
||||
notrunning: `${PREFIX}-notrunning`
|
||||
};
|
||||
|
||||
|
||||
const toggleDeleteStatus = gql`
|
||||
mutation toggleC2ProfileDeleteStatus($translationcontainer_id: Int!, $deleted: Boolean!){
|
||||
update_translationcontainer_by_pk(pk_columns: {id: $translationcontainer_id}, _set: {deleted: $deleted}) {
|
||||
|
||||
@@ -100,6 +100,7 @@ query GetPayloadDetails($payload_id: Int!) {
|
||||
buildparameter {
|
||||
description
|
||||
parameter_type
|
||||
name
|
||||
}
|
||||
}
|
||||
os
|
||||
@@ -109,6 +110,7 @@ query GetPayloadDetails($payload_id: Int!) {
|
||||
c2profileparameter {
|
||||
description
|
||||
parameter_type
|
||||
name
|
||||
}
|
||||
c2profile {
|
||||
name
|
||||
@@ -411,7 +413,7 @@ function DetailedPayloadInnerTable(props){
|
||||
setCommands(commandState);
|
||||
const buildParametersState = data.payload[0].buildparameterinstances.map( (b) =>
|
||||
{
|
||||
return {description: b.buildparameter.description,
|
||||
return {description: b.buildparameter.description, name: b.buildparameter.name,
|
||||
value: b.value,
|
||||
parameter_type: b.buildparameter.parameter_type,
|
||||
enc_key: b.enc_key_base64,
|
||||
@@ -424,6 +426,7 @@ function DetailedPayloadInnerTable(props){
|
||||
if( !(headerName in prev) ){
|
||||
return {...prev, [headerName]:
|
||||
[{description: cur.c2profileparameter.description,
|
||||
name: cur.c2profileparameter.name,
|
||||
value: cur.value,
|
||||
count: cur.count,
|
||||
enc_key: cur.enc_key_base64,
|
||||
@@ -434,6 +437,7 @@ function DetailedPayloadInnerTable(props){
|
||||
}
|
||||
return {...prev, [headerName]: [...prev[headerName],
|
||||
{description: cur.c2profileparameter.description,
|
||||
name: cur.c2profileparameter.name,
|
||||
value: cur.value,
|
||||
count: cur.count,
|
||||
enc_key: cur.enc_key_base64,
|
||||
@@ -445,7 +449,7 @@ function DetailedPayloadInnerTable(props){
|
||||
const c2ProfilesState = Object.keys(c2Profiles).reduce( (prev, cur) => {
|
||||
return [...prev, {
|
||||
c2_profile: cur,
|
||||
parameters: c2Profiles[cur].sort((a,b) => (a.description > b.description) ? 1: ((b.description > a.description) ? -1 : 0))
|
||||
parameters: c2Profiles[cur].sort((a,b) => (a.name > b.name) ? 1: ((b.name > a.name) ? -1 : 0))
|
||||
}];
|
||||
}, []);
|
||||
setC2Profiles(c2ProfilesState);
|
||||
@@ -598,6 +602,10 @@ function DetailedPayloadInnerTable(props){
|
||||
}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>Agent File ID</TableCell>
|
||||
<TableCell>{data.payload[0].filemetum.agent_file_id}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>SHA1</TableCell>
|
||||
<TableCell>{data.payload[0].filemetum.sha1}</TableCell>
|
||||
@@ -664,7 +672,14 @@ function DetailedPayloadInnerTable(props){
|
||||
{
|
||||
buildParameters.map( (cmd, i) => (
|
||||
<TableRow key={"buildprop" + i + "for" + props.payload_id} hover>
|
||||
<TableCell>{cmd.description}</TableCell>
|
||||
<TableCell>
|
||||
<Typography style={{fontWeight: "600"}} >
|
||||
{cmd.name}
|
||||
</Typography>
|
||||
<Typography style={{fontSize: theme.typography.pxToRem(15), marginLeft: "10px"}}>
|
||||
{cmd.description}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ParseForDisplay cmd={cmd} />
|
||||
{cmd.enc_key === null ? null : (<React.Fragment>
|
||||
@@ -726,7 +741,14 @@ function DetailedPayloadInnerTable(props){
|
||||
{
|
||||
c2.parameters.map( (cmd, j) => (
|
||||
<TableRow key={"c2frag" + props.payload_id + c2.c2_profile + j} hover>
|
||||
<TableCell>{cmd.description}</TableCell>
|
||||
<TableCell>
|
||||
<Typography style={{fontWeight: "600"}} >
|
||||
{cmd.name}
|
||||
</Typography>
|
||||
<Typography style={{fontSize: theme.typography.pxToRem(15), marginLeft: "10px"}}>
|
||||
{cmd.description}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ParseForDisplay cmd={cmd} />
|
||||
{cmd.enc_key === null ? null : (<React.Fragment>
|
||||
|
||||
@@ -16,9 +16,11 @@ fragment payloadData on payload {
|
||||
description
|
||||
deleted
|
||||
auto_generated
|
||||
payload_type_semver
|
||||
payloadtype {
|
||||
id
|
||||
name
|
||||
semver
|
||||
}
|
||||
payload_build_steps(order_by: {step_number: asc}) {
|
||||
step_name
|
||||
|
||||
@@ -44,13 +44,14 @@ import {PayloadGetIOCDialog} from "./PayloadGetIOCDialog";
|
||||
import {PayloadGetSampleMessageDialog} from "./PayloadGetSampleMessageDialog";
|
||||
import IosShareIcon from '@mui/icons-material/IosShare';
|
||||
import {TagsDisplay, ViewEditTags} from "../../MythicComponents/MythicTag";
|
||||
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
|
||||
import {MythicAgentSVGIcon, MythicAgentSVGIconNoTooltip} from "../../MythicComponents/MythicAgentSVGIcon";
|
||||
import SmartToyTwoToneIcon from '@mui/icons-material/SmartToyTwoTone';
|
||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||
import BlockIcon from '@mui/icons-material/Block';
|
||||
import NotificationsOffOutlinedIcon from '@mui/icons-material/NotificationsOffOutlined';
|
||||
import {EditPayloadConfigDialog} from "./EditPayloadConfigDialog";
|
||||
import DifferenceIcon from '@mui/icons-material/Difference';
|
||||
import {Dropdown, DropdownMenuItem, DropdownNestedMenuItem} from "../../MythicComponents/MythicNestedMenus";
|
||||
|
||||
const rebuildPayloadMutation = gql`
|
||||
mutation triggerRebuildMutation($uuid: String!) {
|
||||
@@ -146,90 +147,195 @@ export function PayloadsTableRow(props){
|
||||
const onCallbacksAllowedChanged = () =>[
|
||||
props.onCallbacksAllowedChanged(props.uuid, !props.callback_allowed)
|
||||
]
|
||||
const handleMenuItemClick = (event, index) => {
|
||||
options[index].click();
|
||||
const handleMenuItemClick = (event, clickOption) => {
|
||||
clickOption({event});
|
||||
setOpenUpdateDialog(false);
|
||||
};
|
||||
const options = [
|
||||
{name: <><DriveFileRenameOutlineIcon style={{marginRight: "10px"}}/> {"Rename File"}</> , click: () => {
|
||||
setOpenFilenameDialog(true);
|
||||
}},
|
||||
{name: <><DescriptionIcon color={"info"} style={{marginRight: "10px"}} />{'Edit Description'}</>, click: () => {
|
||||
setOpenDescriptionDialog(true);
|
||||
}},
|
||||
{name: <><InfoIconOutline color={"info"} style={{marginRight: "10px"}} /> View Payload Configuration</>, click: () => {
|
||||
setOpenDetailedView(true);
|
||||
}},
|
||||
{name: <><DifferenceIcon color={"info"} style={{marginRight: "10px"}} /> Compare Payload Configuration</>, click: () => {
|
||||
setOpenComparePayloadsDialog(true);
|
||||
}},
|
||||
{name: props.callback_alert ?
|
||||
<><VisibilityIcon color={"success"} style={{marginRight: "10px"}} />{'Alerting to New Callbacks'}</> :
|
||||
<><VisibilityOffIcon color={"error"} style={{marginRight: "10px"}} />{"Not Alerting to New Callbacks"}</>,
|
||||
click: () => {
|
||||
onAlertChanged();
|
||||
}},
|
||||
{name: props.callback_allowed ?
|
||||
<><VisibilityIcon color={"success"} style={{marginRight: "10px"}} />{'Allowing New Callbacks from this Payload'}</> :
|
||||
<><VisibilityOffIcon color={"error"} style={{marginRight: "10px"}} />{"Preventing New Callbacks from this Payload"}</>,
|
||||
click: () => {
|
||||
onCallbacksAllowedChanged();
|
||||
}},
|
||||
{name: <><MessageIcon style={{marginRight: "10px"}} />{'View Build Message/Stdout'} </> , click: () => {
|
||||
setViewError(false);
|
||||
setOpenBuildMessageDialog(true);
|
||||
}},
|
||||
{name: <><ErrorIcon color={"error"} style={{marginRight: "10px"}} />{'View Build Errors'}</>, click: () => {
|
||||
setViewError(true);
|
||||
setOpenBuildMessageDialog(true);
|
||||
}},
|
||||
{name: <><CachedIcon color={"success"} style={{marginRight: "10px"}} />{'Trigger New Build'}</>, click: () => {
|
||||
triggerRebuild({variables: {uuid: props.uuid}});
|
||||
}},
|
||||
{name: <><CachedIcon color={"success"} style={{marginRight: "10px"}} />{'Trigger New Build With Edits'}</>, click: () => {
|
||||
setOpenEditPayloadConfigDialog(true);
|
||||
}},
|
||||
{name: <><IosShareIcon color={"info"} style={{marginRight: "10px"}} />{'Export Payload Config'}</>, click: () => {
|
||||
exportConfig({variables: {uuid: props.uuid}});
|
||||
}},
|
||||
{name: <><PhoneMissedIcon style={{marginRight: "10px"}} />{'Generate Redirect Rules'}</>, click: () => {
|
||||
setOpenRedirectRulesDialog(true);
|
||||
}},
|
||||
{name: <><VerifiedIcon style={{marginRight: "10px"}} />{'Check Agent C2 Configuration'}</>, click: () => {
|
||||
setOpenConfigCheckDialog(true);
|
||||
}},
|
||||
{name: <><FingerprintIcon style={{marginRight: "10px"}} />{'Generate IOCs'}</>, click: () => {
|
||||
setOpenGenerateIOCDialog(true);
|
||||
}},
|
||||
{name: <><BiotechIcon style={{marginRight: "10px"}} />{'Generate Sample Message'}</>, click: () => {
|
||||
setOpenGenerateSampleMessageDialog(true);
|
||||
}},
|
||||
{name: <><AddIcCallIcon color={"success"} style={{marginRight: "10px"}} />{'Generate Fake Callback'}</>, click: () => {
|
||||
setOpenCreateNewCallbackDialog(true);
|
||||
}},
|
||||
{name:<>
|
||||
{props.deleted ? (
|
||||
<>
|
||||
<RestoreFromTrashIcon color={"success"} style={{marginRight: "10px"}} />
|
||||
Restore Payload
|
||||
</>
|
||||
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<DeleteIcon color={"error"} style={{marginRight: "10px"}}/>
|
||||
Delete the payload from disk
|
||||
</React.Fragment>
|
||||
)}</>, click: () => {
|
||||
setOpenDeleteDialog(true);
|
||||
}}
|
||||
]
|
||||
;
|
||||
{
|
||||
name: "Rename File", type: "item",
|
||||
icon: <DriveFileRenameOutlineIcon style={{marginRight: "10px"}}/>,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenFilenameDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Edit Description", type: "item",
|
||||
icon: <DescriptionIcon color={"info"} style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenDescriptionDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "View Payload Configuration", type: "item",
|
||||
icon: <InfoIconOutline color={"info"} style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenDetailedView(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Compare Payload Configuration", type: "item",
|
||||
icon: <DifferenceIcon color={"info"} style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenComparePayloadsDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: props.callback_alert ? "Alerting to New Callbacks" : "Not Alerting to New Callbacks",
|
||||
type: "item",
|
||||
icon: props.callback_alert ?
|
||||
<VisibilityIcon color={"success"} style={{marginRight: "10px"}} />:
|
||||
<VisibilityOffIcon color={"error"} style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onAlertChanged();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: props.callback_allowed ? "Allowing New Callbacks from this Payload" : "Preventing New Callbacks from this Payload",
|
||||
type: "item",
|
||||
icon: props.callback_allowed ?
|
||||
<VisibilityIcon color={"success"} style={{marginRight: "10px"}} /> :
|
||||
<VisibilityOffIcon color={"error"} style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onCallbacksAllowedChanged();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "View Build Message/Stdout",
|
||||
type: "item",
|
||||
icon: <MessageIcon style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setViewError(false);
|
||||
setOpenBuildMessageDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "View Build Errors",
|
||||
type: "item",
|
||||
icon: <ErrorIcon color={"error"} style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setViewError(true);
|
||||
setOpenBuildMessageDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Trigger New Build",
|
||||
type: "item",
|
||||
icon: <CachedIcon color={"success"} style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
triggerRebuild({variables: {uuid: props.uuid}});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Trigger New Build With Edits",
|
||||
type: "item",
|
||||
icon: <CachedIcon color={"success"} style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenEditPayloadConfigDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Export Payload Config",
|
||||
type: "item",
|
||||
icon: <IosShareIcon color={"info"} style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
exportConfig({variables: {uuid: props.uuid}});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Generate Redirect Rules",
|
||||
type: "item",
|
||||
icon: <PhoneMissedIcon style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenRedirectRulesDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Check Agent C2 Configuration",
|
||||
type: "item",
|
||||
icon: <VerifiedIcon style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenConfigCheckDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Generate IOCs",
|
||||
type: "item",
|
||||
icon: <FingerprintIcon style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenGenerateIOCDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Generate Sample Message",
|
||||
type: "item",
|
||||
icon: <BiotechIcon style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenGenerateSampleMessageDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Generate Fake Callback",
|
||||
type: "item",
|
||||
icon: <AddIcCallIcon color={"success"} style={{marginRight: "10px"}} />,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenCreateNewCallbackDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: props.deleted ? "Restore Payload" : "Delete the Payload from Disk",
|
||||
type: "item",
|
||||
icon: props.deleted ? <RestoreFromTrashIcon color={"success"} style={{marginRight: "10px"}} /> :
|
||||
<DeleteIcon color={"error"} style={{marginRight: "10px"}}/>,
|
||||
click: ({event}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenDeleteDialog(true);
|
||||
}
|
||||
}
|
||||
];
|
||||
const handleClose = (event) => {
|
||||
if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
setOpenUpdateDialog(false);
|
||||
};
|
||||
};
|
||||
const openMenu = (event) => {
|
||||
dropdownAnchorRef.current = event?.currentTarget || event.target;
|
||||
setOpenUpdateDialog(true);
|
||||
}
|
||||
|
||||
const shouldDisplay = React.useMemo(() => {
|
||||
if(!props.deleted){
|
||||
@@ -245,35 +351,46 @@ export function PayloadsTableRow(props){
|
||||
<React.Fragment>
|
||||
<TableRow key={"payload" + props.uuid} hover>
|
||||
<MythicStyledTableCell>
|
||||
<Button ref={dropdownAnchorRef} size="small" onClick={()=>{setOpenUpdateDialog(true);}} >
|
||||
<Button size="small" onClick={openMenu} >
|
||||
Actions <ArrowDropDownIcon />
|
||||
</Button>
|
||||
{openUpdate &&
|
||||
<Popper open={openUpdate} anchorEl={dropdownAnchorRef.current} role={undefined} transition disablePortal style={{zIndex: 4}}>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
|
||||
}}
|
||||
>
|
||||
<Paper className={"dropdownMenuColored"}>
|
||||
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
|
||||
<MenuList id="split-button-menu" >
|
||||
{options.map((option, index) => (
|
||||
<MenuItem
|
||||
key={index + props.uuid}
|
||||
onClick={(event) => handleMenuItemClick(event, index)}
|
||||
>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
<Dropdown
|
||||
isOpen={dropdownAnchorRef.current}
|
||||
onOpen={setOpenUpdateDialog}
|
||||
externallyOpen={openUpdate}
|
||||
menu={
|
||||
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>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
}
|
||||
{openDescription &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openDescription}
|
||||
@@ -340,8 +457,12 @@ export function PayloadsTableRow(props){
|
||||
}
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<MythicStyledTooltip title={props.payloadtype.name} tooltipStyle={{display: "inline-block"}}>
|
||||
<MythicAgentSVGIcon payload_type={props.payloadtype.name} style={{width: "35px", height: "35px"}} />
|
||||
<MythicStyledTooltip title={props.payloadtype.name +
|
||||
(props.payload_type_semver === "" ? "" : `\nBuilt w/ Version: v${props.payload_type_semver}`) +
|
||||
(props.payloadtype.semver === "" ? "" : `\nCurrent Version: v${props.payloadtype.semver}`)
|
||||
}
|
||||
tooltipStyle={{display: "inline-block", whiteSpace: "pre-wrap"}}>
|
||||
<MythicAgentSVGIconNoTooltip payload_type={props.payloadtype.name} style={{width: "35px", height: "35px"}} />
|
||||
</MythicStyledTooltip>
|
||||
{!props.callback_allowed &&
|
||||
<MythicStyledTooltip title={"No new Callbacks allowed from this payload"}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import {Button, IconButton, Typography, Link} from '@mui/material';
|
||||
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 { MythicDialog, MythicModifyStringDialog, MythicViewJSONAsTableDialog } from '../../MythicComponents/MythicDialog';
|
||||
import PlaylistAddCheckIcon from '@mui/icons-material/PlaylistAddCheck';
|
||||
import { gql, useMutation } from '@apollo/client';
|
||||
import {snackActions} from '../../utilities/Snackbar';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import MythicStyledTableCell from '../../MythicComponents/MythicTableCell';
|
||||
import {TagsDisplay, ViewEditTags} from '../../MythicComponents/MythicTag';
|
||||
|
||||
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 function CustomBrowserTable({rows, columns, me}){
|
||||
const [entries, setEntries] = React.useState([]);
|
||||
useEffect( () => {
|
||||
setEntries([...rows]);
|
||||
}, [rows]);
|
||||
const onEditComment = ({id, comment}) => {
|
||||
const updates = entries.map( (file) => {
|
||||
if(file.id === id){
|
||||
return {...file, comment}
|
||||
}else{
|
||||
return {...file}
|
||||
}
|
||||
});
|
||||
setEntries(updates);
|
||||
}
|
||||
return (
|
||||
<TableContainer className="mythicElement" style={{height: "100%", overflowY: "auto"}} >
|
||||
<Table stickyHeader size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell >Name</TableCell>
|
||||
{columns.map((column, index) => (
|
||||
<TableCell key={index}>{column.name}</TableCell>
|
||||
))}
|
||||
<TableCell >Comment</TableCell>
|
||||
<TableCell >Tags</TableCell>
|
||||
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{entries.map( (op) => (
|
||||
<CustomBrowserTableRow
|
||||
key={"browser" + op.id}
|
||||
me={me}
|
||||
onEditComment={onEditComment}
|
||||
columns={columns}
|
||||
{...op}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)
|
||||
}
|
||||
function CustomBrowserTableRow(props){
|
||||
const me = props.me;
|
||||
const [viewPermissionsDialogOpen, setViewPermissionsDialogOpen] = React.useState(false);
|
||||
const [editCommentDialogOpen, setEditCommentDialogOpen] = React.useState(false);
|
||||
const [updateComment] = useMutation(updateFileComment, {
|
||||
onCompleted: (data) => {
|
||||
snackActions.success("updated comment");
|
||||
props.onEditComment(data.update_mythictree_by_pk)
|
||||
}
|
||||
});
|
||||
const onSubmitUpdatedComment = (comment) => {
|
||||
updateComment({variables: {mythictree_id: props.id, comment: comment}})
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TableRow hover>
|
||||
{viewPermissionsDialogOpen && <MythicDialog fullWidth={true} maxWidth="md" open={viewPermissionsDialogOpen}
|
||||
onClose={()=>{setViewPermissionsDialogOpen(false);}}
|
||||
innerDialog={<MythicViewJSONAsTableDialog title="View Metadata" leftColumn="Name" rightColumn="Value" value={props.metadata} onClose={()=>{setViewPermissionsDialogOpen(false);}} />}
|
||||
/>
|
||||
}
|
||||
{editCommentDialogOpen && <MythicDialog fullWidth={true} maxWidth="md" open={editCommentDialogOpen}
|
||||
onClose={()=>{setEditCommentDialogOpen(false);}}
|
||||
innerDialog={<MythicModifyStringDialog title="Edit Browser Comment" onSubmit={onSubmitUpdatedComment} value={props.comment} onClose={()=>{setEditCommentDialogOpen(false);}} />}
|
||||
/>
|
||||
}
|
||||
<MythicStyledTableCell>
|
||||
<Button color="info" style={{}} onClick={() => setViewPermissionsDialogOpen(true)}><PlaylistAddCheckIcon /></Button>
|
||||
<Typography variant="body2" style={{wordBreak: "break-all"}}><b>Host: </b> {props.host}</Typography>
|
||||
{props.callback?.mythictree_groups.length > 0 ? (
|
||||
<Typography variant="body2" style={{whiteSpace: "pre"}}>
|
||||
<b>Groups: </b>{props?.callback.mythictree_groups.join(", ")}<br/>
|
||||
<b>Callback: </b>{
|
||||
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
|
||||
href={"/new/callbacks/" + props.callback.display_id}>
|
||||
C-{props.callback.display_id}
|
||||
</Link>
|
||||
}{", "}
|
||||
<b>Task: </b>{
|
||||
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
|
||||
href={"/new/task/" + props.task.display_id}>
|
||||
T-{props.task.display_id}
|
||||
</Link>
|
||||
}
|
||||
</Typography>
|
||||
) : null}
|
||||
<Typography variant="body2" style={{wordBreak: "break-all", textDecoration: props.deleted ? "strike-through" : ""}}>{props.full_path_text}</Typography>
|
||||
|
||||
</MythicStyledTableCell>
|
||||
{props.columns.map((column, index) => (
|
||||
<MythicStyledTableCell key={index}>
|
||||
<Typography variant="body2" style={{wordBreak: "break-all", display: "inline-block"}}>{props.metadata[column.key]}</Typography>
|
||||
</MythicStyledTableCell>
|
||||
))}
|
||||
<MythicStyledTableCell>
|
||||
<IconButton onClick={() => setEditCommentDialogOpen(true)} size="small" style={{display: "inline-block"}}><EditIcon /></IconButton>
|
||||
<Typography variant="body2" style={{wordBreak: "break-all", display: "inline-block"}}>{props.comment}</Typography>
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<ViewEditTags target_object={"mythictree_id"} target_object_id={props.id} me={me} />
|
||||
<TagsDisplay tags={props.tags} />
|
||||
</MythicStyledTableCell>
|
||||
|
||||
</TableRow>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {SearchTabSocksLabel, SearchTabSocksPanel} from './SearchTabProxies';
|
||||
import {SearchTabProcessesLabel, SearchTabProcessPanel} from "./SearchTabProcesses";
|
||||
import {SearchTabTagsLabel, SearchTabTagsPanel} from "./SearchTabTags";
|
||||
import {SearchTabPayloadsLabel, SearchTabPayloadsPanel} from "./SearchTabPayloads";
|
||||
import {SearchTabCustomBrowserLabel, SearchTabCustomBrowserPanel} from "./SearchTabCustomBrowsers";
|
||||
|
||||
const PREFIX = 'Search';
|
||||
|
||||
@@ -34,7 +35,7 @@ const Root = styled('div')((
|
||||
export function Search(props){
|
||||
|
||||
const navigate = useNavigate();
|
||||
const tabTypes = ["callbacks", "tasks", "payloads", "files", "credentials", "keylogs", "artifacts", "tokens", "socks", "processes", "tags"];
|
||||
const tabTypes = ["callbacks", "tasks", "payloads", "files", "credentials", "keylogs", "artifacts", "tokens", "socks", "processes", "tags", "browsers"];
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var valueString = params.get("tab") ? params.get("tab") : tabTypes[0];
|
||||
var valueIndex = tabTypes.findIndex(t => t === valueString);
|
||||
@@ -74,6 +75,8 @@ export function Search(props){
|
||||
return <SearchTabProcessPanel key={"processpanel"} index={value} me={props.me} value={value} changeSearchParam={changeSearchParam} />
|
||||
case "tags":
|
||||
return <SearchTabTagsPanel key={"tagspanel"} index={value} me={props.me} value={value} changeSearchParam={changeSearchParam} />
|
||||
case "browsers":
|
||||
return <SearchTabCustomBrowserPanel key={"browserpanel"} index={value} me={props.me} value={value} changeSearchParam={changeSearchParam} />
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -115,6 +118,8 @@ export function Search(props){
|
||||
return <SearchTabProcessesLabel key={"processtab"} me={props.me}/>;
|
||||
case "tags":
|
||||
return <SearchTabTagsLabel key={"tagstab"} me={props.me} />;
|
||||
case "browsers":
|
||||
return <SearchTabCustomBrowserLabel key={"browsers"} me={props.me}/>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,417 @@
|
||||
import {MythicTabPanel, MythicSearchTabLabel} from '../../MythicComponents/MythicTabPanel';
|
||||
import React from 'react';
|
||||
import MythicTextField from '../../MythicComponents/MythicTextField';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { gql, useQuery} from '@apollo/client';
|
||||
import { snackActions } from '../../utilities/Snackbar';
|
||||
import Pagination from '@mui/material/Pagination';
|
||||
import { Typography } from '@mui/material';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Select from '@mui/material/Select';
|
||||
import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery";
|
||||
import {CustomBrowserTable} from "./CustomBrowserTable";
|
||||
|
||||
|
||||
const mythictreeFragment = gql`
|
||||
fragment mythictreeSearchData on mythictree{
|
||||
comment
|
||||
deleted
|
||||
full_path_text
|
||||
host
|
||||
id
|
||||
name_text
|
||||
metadata
|
||||
task {
|
||||
display_id
|
||||
}
|
||||
callback {
|
||||
mythictree_groups
|
||||
id
|
||||
display_id
|
||||
}
|
||||
tags {
|
||||
tagtype {
|
||||
name
|
||||
color
|
||||
id
|
||||
}
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
const nameSearch = gql`
|
||||
${mythictreeFragment}
|
||||
query nameCustomBrowserQuery($name: String!, $host: String!, $offset: Int!, $fetchLimit: Int!, $tree_type: String!) {
|
||||
mythictree_aggregate(distinct_on: id, where: {full_path_text: {_ilike: $name}, host: {_ilike: $host}, tree_type: {_eq: $tree_type}}) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
mythictree(limit: $fetchLimit, distinct_on: id, offset: $offset, order_by: {id: desc}, where: {host: {_ilike: $host}, full_path_text: {_ilike: $name}, tree_type: {_eq: $tree_type}}) {
|
||||
...mythictreeSearchData
|
||||
}
|
||||
}
|
||||
`;
|
||||
const metadataSearch = gql`
|
||||
${mythictreeFragment}
|
||||
query metadataCustomBrowserQuery($search: String!, $host: String!, $offset: Int!, $fetchLimit: Int!, $tree_type: String!) {
|
||||
mythictree_aggregate(distinct_on: id, where: {metadata: {_cast: {String: {_ilike: $search}}}, host: {_ilike: $host}, tree_type: {_eq: $tree_type}}) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
mythictree(limit: $fetchLimit, distinct_on: id, offset: $offset, order_by: {id: desc}, where: {host: {_ilike: $host}, metadata: {_cast: {String: {_ilike: $search}}}, tree_type: {_eq: $tree_type}}) {
|
||||
...mythictreeSearchData
|
||||
}
|
||||
}
|
||||
`;
|
||||
const commentSearch = gql`
|
||||
${mythictreeFragment}
|
||||
query nameCustomBrowserQuery($comment: String!, $host: String!, $offset: Int!, $fetchLimit: Int!, $tree_type: String!) {
|
||||
mythictree_aggregate(distinct_on: id, where: {comment: {_ilike: $comment}, host: {_ilike: $host}, tree_type: {_eq: $tree_type}}) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
mythictree(limit: $fetchLimit, distinct_on: id, offset: $offset, order_by: {id: desc}, where: {host: {_ilike: $host}, comment: {_ilike: $comment}, tree_type: {_eq: $tree_type}}) {
|
||||
...mythictreeSearchData
|
||||
}
|
||||
}
|
||||
`;
|
||||
const tagSearch = gql`
|
||||
${mythictreeFragment}
|
||||
query tagCustomBrowserQuery($tag: String!, $host: String!, $offset: Int!, $fetchLimit: Int!, $tree_type: String!) {
|
||||
tag_aggregate(distinct_on: mythictree_id, where: {mythictree_id: {_is_null: false}, _or: [{data: {_cast: {String: {_ilike: $tag}}}}, {tagtype: {name: {_ilike: $tag}}}], mythictree: {host: {_ilike: $host}, tree_type: {_eq: $tree_type}}}) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
tag(limit: $fetchLimit, distinct_on: mythictree_id, offset: $offset, order_by: {mythictree_id: desc}, where: {mythictree_id: {_is_null: false}, _or: [{data: {_cast: {String: {_ilike: $tag}}}}, {tagtype: {name: {_ilike: $tag}}}], mythictree: {host: {_ilike: $host}, tree_type: {_eq: $tree_type}}}) {
|
||||
mythictree {
|
||||
...mythictreeSearchData
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
`;
|
||||
const customBrowsers = gql`
|
||||
query getCustomBrowsers {
|
||||
custombrowser(where: {deleted: {_eq: false}}, order_by: {name: asc}){
|
||||
name
|
||||
columns
|
||||
}
|
||||
}
|
||||
`;
|
||||
const fetchLimit = 100;
|
||||
|
||||
export function SearchTabCustomBrowserLabel(props){
|
||||
return (
|
||||
<MythicSearchTabLabel label={"Browsers"} iconComponent={<FolderIcon />} {...props}/>
|
||||
)
|
||||
}
|
||||
const SearchTabCustomBrowserSearchPanel = (props) => {
|
||||
const theme = useTheme();
|
||||
const [browserOptions, setBrowserOptions] = React.useState([]);
|
||||
const [selectedBrowser, setSelectedBrowser] = React.useState("");
|
||||
const [search, setSearch] = React.useState("");
|
||||
const [searchHost, setSearchHost] = React.useState("");
|
||||
const [searchField, setSearchField] = React.useState("Name");
|
||||
const [searchFieldOptions, setSearchFieldOptions] = React.useState(["Name", "Tags", "Comment", "Metadata"]);
|
||||
const handleBrowserChange = (event) => {
|
||||
setSelectedBrowser(event.target.value);
|
||||
for(let i = 0; i < browserOptions.length; i++) {
|
||||
if(browserOptions[i].name === event.target.value){
|
||||
props.onChangeBrowserField(browserOptions[i]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
const handleSearchFieldChange = (event) => {
|
||||
setSearchField(event.target.value);
|
||||
props.onChangeSearchField(event.target.value);
|
||||
props.changeSearchParam("searchField", event.target.value);
|
||||
}
|
||||
const handleSearchHostValueChange = (name, value, error) => {
|
||||
setSearchHost(value);
|
||||
}
|
||||
const handleSearchValueChange = (name, value, error) => {
|
||||
setSearch(value);
|
||||
}
|
||||
const submitSearch = (event, querySearch, querySearchHost, querySearchField) => {
|
||||
let adjustedSearchField = querySearchField ? querySearchField : searchField;
|
||||
let adjustedSearch = querySearch ? querySearch : search;
|
||||
let adjustedSearchHost = querySearchHost ? querySearchHost : searchHost;
|
||||
props.changeSearchParam("host", adjustedSearchHost);
|
||||
props.changeSearchParam("search", adjustedSearch);
|
||||
switch(adjustedSearchField){
|
||||
case "Name":
|
||||
props.onNameSearch({search:adjustedSearch, searchHost:adjustedSearchHost, offset: 0})
|
||||
break;
|
||||
case "Comment":
|
||||
props.onCommentSearch({search:adjustedSearch, searchHost:adjustedSearchHost, offset: 0})
|
||||
break;
|
||||
case "Tag":
|
||||
props.onTagSearch({search:adjustedSearch, searchHost:adjustedSearchHost, offset: 0});
|
||||
break;
|
||||
case "Metadata":
|
||||
props.onOtherSearch({search:adjustedSearch, searchHost:adjustedSearchHost, offset: 0});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
useQuery(customBrowsers, {fetchPolicy: "no-cache",
|
||||
onCompleted: (data) => {
|
||||
setBrowserOptions(data.custombrowser);
|
||||
if(data.custombrowser.length > 0){
|
||||
setSelectedBrowser(data.custombrowser[0].name);
|
||||
props.onChangeBrowserField(data.custombrowser[0]);
|
||||
}
|
||||
}
|
||||
})
|
||||
React.useEffect(() => {
|
||||
if(props.value === props.index){
|
||||
let queryParams = new URLSearchParams(window.location.search);
|
||||
let adjustedSearch = "";
|
||||
let adjustedSearchHost = "";
|
||||
let adjustedSearchField = "Name";
|
||||
if(queryParams.has("search")){
|
||||
setSearch(queryParams.get("search"));
|
||||
adjustedSearch = queryParams.get("search");
|
||||
}
|
||||
if(queryParams.has("searchField") && searchFieldOptions.includes(queryParams.get("searchField"))){
|
||||
setSearchField(queryParams.get("searchField"));
|
||||
props.onChangeSearchField(queryParams.get("searchField"));
|
||||
adjustedSearchField = queryParams.get("searchField");
|
||||
}else{
|
||||
setSearchField("Name");
|
||||
props.onChangeSearchField("Name");
|
||||
props.changeSearchParam("searchField", "Name");
|
||||
}
|
||||
if(queryParams.has("host")){
|
||||
setSearchHost(queryParams.get("host"));
|
||||
adjustedSearchHost = queryParams.get("host")
|
||||
}
|
||||
submitSearch(null, adjustedSearch, adjustedSearchHost, adjustedSearchField);
|
||||
}
|
||||
}, [props.value, props.index]);
|
||||
return (
|
||||
<Grid container spacing={1} style={{padding: "5px 5px 0 5px", maxWidth: "100%"}}>
|
||||
<Grid size={2}>
|
||||
<Select
|
||||
style={{marginBottom: "10px", width: "100%"}}
|
||||
value={selectedBrowser}
|
||||
onChange={handleBrowserChange}
|
||||
>
|
||||
{
|
||||
browserOptions.map((opt, i) => (
|
||||
<MenuItem key={"searchopt" + opt.name} value={opt.name}>{opt.name}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
</Grid>
|
||||
<Grid size={2}>
|
||||
<MythicTextField placeholder="Host Name Search..." value={searchHost} marginTop={"0px"}
|
||||
onChange={handleSearchHostValueChange} onEnter={submitSearch} name="Host Name Search..." />
|
||||
</Grid>
|
||||
<Grid size={3}>
|
||||
<MythicTextField placeholder="Search..." value={search} marginTop={"0px"}
|
||||
onChange={handleSearchValueChange} onEnter={submitSearch} name="Search..." InputProps={{
|
||||
endAdornment:
|
||||
<React.Fragment>
|
||||
<Tooltip title="Search">
|
||||
<IconButton onClick={submitSearch} size="large"><SearchIcon style={{color: theme.palette.info.main}}/></IconButton>
|
||||
</Tooltip>
|
||||
</React.Fragment>,
|
||||
style: {padding: 0}
|
||||
}}/>
|
||||
</Grid>
|
||||
<Grid size={2}>
|
||||
<Select
|
||||
style={{marginBottom: "10px", width: "100%"}}
|
||||
value={searchField}
|
||||
onChange={handleSearchFieldChange}
|
||||
>
|
||||
{
|
||||
searchFieldOptions.map((opt, i) => (
|
||||
<MenuItem key={"searchopt" + opt} value={opt}>{opt}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
export const SearchTabCustomBrowserPanel = (props) =>{
|
||||
const [processData, setProcessData] = React.useState([]);
|
||||
const [totalCount, setTotalCount] = React.useState(0);
|
||||
const [selectedBrowser, setSelectedBrowser] = React.useState({name: "", columns: []});
|
||||
const [search, setSearch] = React.useState("");
|
||||
const [searchField, setSearchField] = React.useState("Name");
|
||||
const [searchHost, setSearchHost] = React.useState("");
|
||||
const onChangeBrowserField = (field) => {
|
||||
setSelectedBrowser(field);
|
||||
setProcessData([]);
|
||||
}
|
||||
const onChangeSearchField = (field) => {
|
||||
setSearchField(field);
|
||||
setProcessData([]);
|
||||
switch(field){
|
||||
case "Name":
|
||||
onNameSearch({search, searchHost, offset: 0});
|
||||
break;
|
||||
case "Comment":
|
||||
onCommentSearch({search, searchHost, offset: 0});
|
||||
break;
|
||||
case "Tag":
|
||||
onTagSearch({search, searchHost, offset: 0});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
const handleSearchResults = (data) => {
|
||||
snackActions.dismiss();
|
||||
if(searchField === "Tag"){
|
||||
setTotalCount(data.tag_aggregate.aggregate.count);
|
||||
setProcessData(data?.tag?.map(t => t.mythictree) || []);
|
||||
} else {
|
||||
setTotalCount(data.mythictree_aggregate.aggregate.count);
|
||||
setProcessData(data.mythictree);
|
||||
}
|
||||
|
||||
}
|
||||
const handleCallbackSearchFailure = (data) => {
|
||||
snackActions.dismiss();
|
||||
snackActions.error("Failed to fetch data for search");
|
||||
console.log(data);
|
||||
}
|
||||
const getNameSearch = useMythicLazyQuery(nameSearch, {
|
||||
fetchPolicy: "no-cache"
|
||||
})
|
||||
const getCommentSearch = useMythicLazyQuery(commentSearch, {
|
||||
fetchPolicy: "no-cache",
|
||||
})
|
||||
const getTagSearch = useMythicLazyQuery(tagSearch, {
|
||||
fetchPolicy: "no-cache"
|
||||
})
|
||||
const getOtherSearch = useMythicLazyQuery(metadataSearch, {fetchPolicy: "no-cache"})
|
||||
const onNameSearch = ({search, searchHost, offset}) => {
|
||||
//snackActions.info("Searching...", {persist:true});
|
||||
setSearch(search);
|
||||
if(selectedBrowser.name === ""){return}
|
||||
let new_search = search;
|
||||
if(new_search === ""){
|
||||
new_search = "_";
|
||||
}
|
||||
setSearchHost(searchHost);
|
||||
getNameSearch({variables:{
|
||||
offset: offset,
|
||||
fetchLimit: fetchLimit,
|
||||
name: "%" + new_search + "%",
|
||||
host: "%" + searchHost + "%",
|
||||
tree_type: selectedBrowser.name
|
||||
}}).then(({data}) => handleSearchResults(data)).catch(({data}) => handleCallbackSearchFailure(data))
|
||||
}
|
||||
const onCommentSearch = ({search, searchHost, offset}) => {
|
||||
//snackActions.info("Searching...", {persist:true});
|
||||
setSearch(search);
|
||||
if(selectedBrowser.name === ""){return}
|
||||
let new_search = search;
|
||||
if(new_search === ""){
|
||||
new_search = "_";
|
||||
}
|
||||
setSearchHost(searchHost);
|
||||
getCommentSearch({variables:{
|
||||
offset: offset,
|
||||
fetchLimit: fetchLimit,
|
||||
comment: "%" + new_search + "%",
|
||||
host: "%" + searchHost + "%",
|
||||
tree_type: selectedBrowser.name
|
||||
}}).then(({data}) => handleSearchResults(data)).catch(({data}) => handleCallbackSearchFailure(data))
|
||||
}
|
||||
const onTagSearch = ({search, searchHost, offset}) => {
|
||||
//snackActions.info("Searching...", {persist:true});
|
||||
setSearch(search);
|
||||
if(selectedBrowser.name === ""){return}
|
||||
let new_search = search;
|
||||
if(new_search === ""){
|
||||
new_search = "_";
|
||||
}
|
||||
setSearchHost(searchHost);
|
||||
getTagSearch({variables:{
|
||||
offset: offset,
|
||||
fetchLimit: fetchLimit,
|
||||
tag: "%" + new_search + "%",
|
||||
host: "%" + searchHost + "%",
|
||||
tree_type: selectedBrowser.name
|
||||
}}).then(({data}) => handleSearchResults(data)).catch(({data}) => handleCallbackSearchFailure(data))
|
||||
}
|
||||
const onOtherSearch = ({search, searchHost, offset}) => {
|
||||
//snackActions.info("Searching...", {persist:true});
|
||||
setSearch(search);
|
||||
if(selectedBrowser.name === ""){return}
|
||||
let new_search = search;
|
||||
if(new_search === ""){
|
||||
new_search = "_";
|
||||
}
|
||||
setSearchHost(searchHost);
|
||||
getOtherSearch({variables:{
|
||||
offset: offset,
|
||||
fetchLimit: fetchLimit,
|
||||
search: "%" + new_search + "%",
|
||||
host: "%" + searchHost + "%",
|
||||
tree_type: selectedBrowser.name
|
||||
}}).then(({data}) => handleSearchResults(data)).catch(({data}) => handleCallbackSearchFailure(data))
|
||||
}
|
||||
const onChangePage = (event, value) => {
|
||||
switch(searchField){
|
||||
case "Name":
|
||||
onNameSearch({search, searchHost:searchHost, offset: (value - 1) * fetchLimit});
|
||||
break;
|
||||
case "Comment":
|
||||
onCommentSearch({search, searchHost:searchHost, offset: (value - 1) * fetchLimit});
|
||||
break;
|
||||
case "Tag":
|
||||
onTagSearch({search, searchHost:searchHost, offset: (value - 1) * fetchLimit});
|
||||
break;
|
||||
case "Metadata":
|
||||
onOtherSearch({search, searchHost:searchHost, offset: (value - 1) * fetchLimit});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
React.useEffect( () => {
|
||||
if(selectedBrowser.name === ""){return}
|
||||
onChangePage(null, 1);
|
||||
}, [selectedBrowser]);
|
||||
return (
|
||||
<MythicTabPanel {...props} >
|
||||
<SearchTabCustomBrowserSearchPanel onChangeSearchField={onChangeSearchField}
|
||||
onNameSearch={onNameSearch} value={props.value} index={props.index}
|
||||
onCommentSearch={onCommentSearch}
|
||||
onTagSearch={onTagSearch}
|
||||
onOtherSearch={onOtherSearch}
|
||||
onChangeBrowserField={onChangeBrowserField}
|
||||
changeSearchParam={props.changeSearchParam}/>
|
||||
<div style={{overflowY: "auto", flexGrow: 1}}>
|
||||
{processData.length > 0 ? (
|
||||
<CustomBrowserTable rows={processData} columns={selectedBrowser.columns} />) : (
|
||||
<div style={{display: "flex", justifyContent: "center", alignItems: "center", position: "absolute", left: "50%", top: "50%"}}>No Search Results</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{background: "transparent", display: "flex", justifyContent: "center", alignItems: "center"}}>
|
||||
<Pagination count={Math.ceil(totalCount / fetchLimit)} variant="outlined" color="info" boundaryCount={1}
|
||||
siblingCount={1} onChange={onChangePage} showFirstButton={true} showLastButton={true} style={{padding: "20px"}}/>
|
||||
<Typography style={{paddingLeft: "10px"}}>Total Results: {totalCount}</Typography>
|
||||
</div>
|
||||
</MythicTabPanel>
|
||||
)
|
||||
}
|
||||
@@ -24,12 +24,16 @@ fragment tokenData on token{
|
||||
token_id
|
||||
task {
|
||||
id
|
||||
display_id
|
||||
}
|
||||
host
|
||||
deleted
|
||||
description
|
||||
callbacktokens(where: {deleted: {_eq: false}}) {
|
||||
callback_id
|
||||
callbacktokens {
|
||||
callback {
|
||||
display_id
|
||||
id
|
||||
}
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,15 +181,15 @@ function TokenTableRow(props){
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
|
||||
href={"/new/task/" + props.task.id}>
|
||||
T-{props.task.id}
|
||||
href={"/new/task/" + props.task.display_id}>
|
||||
T-{props.task.display_id}
|
||||
</Link>
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>{props.callbacktokens?.map( (cbt) => (
|
||||
<React.Fragment key={"callbacktoken-" + props.id + "-" + cbt.id}>
|
||||
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank" key={"callbacklink" + cbt.callback_id + "row" + props.id}
|
||||
href={"/new/callbacks/" + cbt.callback_id}>
|
||||
C-{cbt.callback_id}
|
||||
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank" key={"callbacklink" + cbt.callback.display_id + "row" + props.id}
|
||||
href={"/new/callbacks/" + cbt.callback.display_id}>
|
||||
C-{cbt.callback.display_id}
|
||||
</Link>
|
||||
{" "}
|
||||
</React.Fragment>
|
||||
|
||||
@@ -129,6 +129,9 @@ export function SettingsOperatorUIConfigDialog(props) {
|
||||
const initialTaskingContextField = GetMythicSetting({setting_name: "taskingContextFields", default_value: operatorSettingDefaults.taskingContextFields});
|
||||
const [taskingContextFields, setTaskingContextFields] = React.useState(initialTaskingContextField);
|
||||
|
||||
const initialShowOPSECBypassUsername = GetMythicSetting({setting_name: "showOPSECBypassUsername", default_value: operatorSettingDefaults.showOPSECBypassUsername});
|
||||
const [showOPSECBypassUsername, setShowOPSECBypassUsername] = React.useState(initialShowOPSECBypassUsername);
|
||||
|
||||
const initialPalette = GetMythicSetting({setting_name: 'palette', default_value: operatorSettingDefaults.palette});
|
||||
const [palette, setPalette] = React.useState({
|
||||
primary: {
|
||||
@@ -203,6 +206,10 @@ export function SettingsOperatorUIConfigDialog(props) {
|
||||
dark: isValidColor(initialPalette?.navBarColor?.dark) ? initialPalette?.navBarColor?.dark : operatorSettingDefaults.palette.navBarColor.dark,
|
||||
light: isValidColor(initialPalette?.navBarColor?.light) ? initialPalette?.navBarColor?.light : operatorSettingDefaults.palette.navBarColor.light,
|
||||
},
|
||||
navBarBottomColor: {
|
||||
dark: isValidColor(initialPalette?.navBarBottomColor?.dark) ? initialPalette?.navBarBottomColor?.dark : operatorSettingDefaults.palette.navBarBottomColor.dark,
|
||||
light: isValidColor(initialPalette?.navBarBottomColor?.light) ? initialPalette?.navBarBottomColor?.light : operatorSettingDefaults.palette.navBarBottomColor.light,
|
||||
},
|
||||
taskPromptTextColor: {
|
||||
dark: isValidColor(initialPalette?.taskPromptTextColor?.dark) ? initialPalette?.taskPromptTextColor?.dark : operatorSettingDefaults.palette.taskPromptTextColor.dark,
|
||||
light: isValidColor(initialPalette?.taskPromptTextColor?.light) ? initialPalette?.taskPromptTextColor?.light : operatorSettingDefaults.palette.taskPromptTextColor.light,
|
||||
@@ -223,6 +230,10 @@ export function SettingsOperatorUIConfigDialog(props) {
|
||||
dark: isValidColor(initialPalette?.taskContextExtraColor?.dark) ? initialPalette?.taskContextExtraColor?.dark : operatorSettingDefaults.palette.taskContextExtraColor.dark,
|
||||
light: isValidColor(initialPalette?.taskContextExtraColor?.light) ? initialPalette?.taskContextExtraColor?.light : operatorSettingDefaults.palette.taskContextExtraColor.light,
|
||||
},
|
||||
emptyFolderColor: {
|
||||
dark: isValidColor(initialPalette?.emptyFolderColor?.dark) ? initialPalette?.emptyFolderColor?.dark : operatorSettingDefaults.palette.emptyFolderColor.dark,
|
||||
light: isValidColor(initialPalette?.emptyFolderColor?.light) ? initialPalette?.emptyFolderColor?.light : operatorSettingDefaults.palette.emptyFolderColor.light,
|
||||
},
|
||||
});
|
||||
const paletteOptionsSolidColor = [
|
||||
{name: "primary", display: "Primary"},
|
||||
@@ -244,7 +255,8 @@ export function SettingsOperatorUIConfigDialog(props) {
|
||||
{name: "background", display: "Background"},
|
||||
{name: "taskContextColor", display: "Tasking Context Generic Background Color"},
|
||||
{name: "taskContextImpersonationColor", display: "Tasking Context User Background Color"},
|
||||
{name: "taskContextExtraColor", display: "Tasking Context Extra Info Background Color"}
|
||||
{name: "taskContextExtraColor", display: "Tasking Context Extra Info Background Color"},
|
||||
{name: "emptyFolderColor", display: "Color of the empty folder icon and text in file-based browsers"}
|
||||
]
|
||||
const [resumeNotifications, setResumeNotifications] = React.useState(false);
|
||||
const [_, updateSettings, clearSettings] = useSetMythicSetting();
|
||||
@@ -263,6 +275,9 @@ export function SettingsOperatorUIConfigDialog(props) {
|
||||
const onHideBrowserTaskingChanged = (evt) => {
|
||||
setHideBrowserTasking(!hideBrowserTasking);
|
||||
}
|
||||
const onShowOPSECBypassUsernameChanged = (evt) => {
|
||||
setShowOPSECBypassUsername(!showOPSECBypassUsername);
|
||||
}
|
||||
const onHideTaskingContextChanged = (evt) => {
|
||||
setHideTaskingContext(!hideTaskingContext);
|
||||
}
|
||||
@@ -315,6 +330,7 @@ export function SettingsOperatorUIConfigDialog(props) {
|
||||
hideBrowserTasking,
|
||||
hideTaskingContext,
|
||||
taskingContextFields,
|
||||
showOPSECBypassUsername,
|
||||
palette: palette
|
||||
}});
|
||||
snackActions.success("updating settings");
|
||||
@@ -340,6 +356,7 @@ export function SettingsOperatorUIConfigDialog(props) {
|
||||
setTaskTimestampDisplayField(operatorSettingDefaults.taskTimestampDisplayField);
|
||||
setHideTaskingContext(operatorSettingDefaults.hideTaskingContext);
|
||||
setTaskingContextFields(operatorSettingDefaults.taskingContextFields);
|
||||
setShowOPSECBypassUsername(operatorSettingDefaults.showOPSECBypassUsername);
|
||||
}
|
||||
const clearAllUserSettings = () => {
|
||||
clearSettings();
|
||||
@@ -358,7 +375,24 @@ export function SettingsOperatorUIConfigDialog(props) {
|
||||
const contents = e.target.result;
|
||||
try{
|
||||
let jsonData = JSON.parse(String(contents));
|
||||
updateSettings({settings: jsonData});
|
||||
let currentSettings = {
|
||||
hideUsernames,
|
||||
showIP,
|
||||
showHostname,
|
||||
showCallbackGroups,
|
||||
fontSize: parseInt(fontSize),
|
||||
fontFamily,
|
||||
showMedia,
|
||||
interactType,
|
||||
useDisplayParamsForCLIHistory,
|
||||
taskTimestampDisplayField,
|
||||
hideBrowserTasking,
|
||||
hideTaskingContext,
|
||||
taskingContextFields,
|
||||
showOPSECBypassUsername,
|
||||
palette: palette
|
||||
}
|
||||
updateSettings({settings: {...currentSettings, ...jsonData}});
|
||||
snackActions.info("Updating settings");
|
||||
props.onClose();
|
||||
}catch(error){
|
||||
@@ -574,6 +608,18 @@ export function SettingsOperatorUIConfigDialog(props) {
|
||||
/>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<MythicStyledTableCell>Show OPSEC Bypass Approvers</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<Switch
|
||||
checked={showOPSECBypassUsername}
|
||||
onChange={onShowOPSECBypassUsernameChanged}
|
||||
color="info"
|
||||
inputProps={{ 'aria-label': 'info checkbox' }}
|
||||
name="showOPSECBypassUsername"
|
||||
/>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<MythicStyledTableCell>Hide Tasking Context Tabs</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
@@ -632,6 +678,29 @@ export function SettingsOperatorUIConfigDialog(props) {
|
||||
</div>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<MythicStyledTableCell>Navigation Bar Bottom Color</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<div style={{display: "flex", width: "100%", paddingRight: "15px"}}>
|
||||
<div style={{display: "inline-block", width: "100%"}}>
|
||||
<HexColorPicker style={{width: "100%"}} color={palette?.navBarBottomColor?.dark} onChange={(v) => onChangePaletteColor("navBarBottomColor", "dark", v)}/>
|
||||
<HexColorInput color={palette?.navBarBottomColor?.dark} onChange={(v) => onChangePaletteColor("navBarBottomColor", "dark", v)}/>
|
||||
<Box sx={{width: "100%", height: 25, backgroundColor: palette?.navBarBottomColor?.dark, display: "flex", alignItems: "center"}}>
|
||||
<PhoneCallbackIcon style={{color: palette.navBarIcons.dark, marginRight: "5px"}}/>
|
||||
<Typography style={{color: palette.navBarText.dark, display: "inline-block"}}>Dark Mode Color</Typography>
|
||||
</Box>
|
||||
</div>
|
||||
<div style={{display: "inline-block", width: "100%"}}>
|
||||
<HexColorPicker style={{width: "100%"}} color={palette?.navBarBottomColor?.light} onChange={(v) => onChangePaletteColor("navBarBottomColor", "light", v)}/>
|
||||
<HexColorInput color={palette?.navBarBottomColor?.light} onChange={(v) => onChangePaletteColor("navBarBottomColor", "light", v)}/>
|
||||
<Box sx={{width: "100%", height: 25, backgroundColor: palette?.navBarBottomColor?.light, display: "flex", alignItems: "center"}}>
|
||||
<PhoneCallbackIcon style={{color: palette.navBarIcons.light, marginRight: "5px"}}/>
|
||||
<Typography style={{color: palette.navBarText.light, display: "inline-block"}}>Light Mode Color</Typography>
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<MythicStyledTableCell>Navigation Bar Icon Colors</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
@@ -639,7 +708,7 @@ export function SettingsOperatorUIConfigDialog(props) {
|
||||
<div style={{display: "inline-block", width: "100%"}}>
|
||||
<HexColorPicker style={{width: "100%"}} color={palette?.navBarIcons?.dark} onChange={(v) => onChangePaletteColor("navBarIcons", "dark", v)}/>
|
||||
<HexColorInput color={palette?.navBarIcons?.dark} onChange={(v) => onChangePaletteColor("navBarIcons", "dark", v)}/>
|
||||
<Box sx={{width: "100%", height: 25, backgroundColor: palette?.navBarColor?.dark, display: "flex", alignItems: "center"}}>
|
||||
<Box sx={{width: "100%", height: 25, background: `linear-gradient(.25turn, ${palette?.navBarColor?.dark}, ${palette?.navBarBottomColor?.dark})`, display: "flex", alignItems: "center"}}>
|
||||
<PhoneCallbackIcon style={{color: palette.navBarIcons.dark, marginRight: "5px"}}/>
|
||||
<Typography style={{color: palette.navBarText.dark, display: "inline-block"}}>Dark Mode Color</Typography>
|
||||
</Box>
|
||||
@@ -647,7 +716,7 @@ export function SettingsOperatorUIConfigDialog(props) {
|
||||
<div style={{display: "inline-block", width: "100%"}}>
|
||||
<HexColorPicker style={{width: "100%"}} color={palette?.navBarIcons?.light} onChange={(v) => onChangePaletteColor("navBarIcons", "light", v)}/>
|
||||
<HexColorInput color={palette?.navBarIcons?.light} onChange={(v) => onChangePaletteColor("navBarIcons", "light", v)}/>
|
||||
<Box sx={{width: "100%", height: 25, backgroundColor: palette?.navBarColor?.light, display: "flex", alignItems: "center"}}>
|
||||
<Box sx={{width: "100%", height: 25, background: `linear-gradient(.25turn, ${palette?.navBarColor?.light}, ${palette?.navBarBottomColor?.light})`, display: "flex", alignItems: "center"}}>
|
||||
<PhoneCallbackIcon style={{color: palette.navBarIcons.light, marginRight: "5px"}}/>
|
||||
<Typography style={{color: palette.navBarText.light, display: "inline-block"}}>Light Mode Color</Typography>
|
||||
</Box>
|
||||
@@ -662,7 +731,7 @@ export function SettingsOperatorUIConfigDialog(props) {
|
||||
<div style={{display: "inline-block", width: "100%"}}>
|
||||
<HexColorPicker style={{width: "100%"}} color={palette?.navBarText?.dark} onChange={(v) => onChangePaletteColor("navBarText", "dark", v)}/>
|
||||
<HexColorInput color={palette?.navBarText?.dark} onChange={(v) => onChangePaletteColor("navBarText", "dark", v)}/>
|
||||
<Box sx={{width: "100%", height: 25, backgroundColor: palette?.navBarColor?.dark, display: "flex", alignItems: "center"}}>
|
||||
<Box sx={{width: "100%", height: 25, background: `linear-gradient(.25turn, ${palette?.navBarColor?.dark}, ${palette?.navBarBottomColor?.dark})`, display: "flex", alignItems: "center"}}>
|
||||
<PhoneCallbackIcon style={{color: palette.navBarIcons.dark, marginRight: "5px"}}/>
|
||||
<Typography style={{color: palette.navBarText.dark, display: "inline-block"}}>Dark Mode Color</Typography>
|
||||
</Box>
|
||||
@@ -670,7 +739,7 @@ export function SettingsOperatorUIConfigDialog(props) {
|
||||
<div style={{display: "inline-block", width: "100%"}}>
|
||||
<HexColorPicker style={{width: "100%"}} color={palette?.navBarText?.light} onChange={(v) => onChangePaletteColor("navBarText", "light", v)}/>
|
||||
<HexColorInput color={palette?.navBarText?.light} onChange={(v) => onChangePaletteColor("navBarText", "light", v)}/>
|
||||
<Box sx={{width: "100%", height: 25, backgroundColor: palette?.navBarColor?.light, display: "flex", alignItems: "center"}}>
|
||||
<Box sx={{width: "100%", height: 25, background: `linear-gradient(.25turn, ${palette?.navBarColor?.light}, ${palette?.navBarBottomColor?.light})`, display: "flex", alignItems: "center"}}>
|
||||
<PhoneCallbackIcon style={{color: palette.navBarIcons.light, marginRight: "5px"}}/>
|
||||
<Typography style={{color: palette.navBarText.light, display: "inline-block"}}>Light Mode Color</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -70,7 +70,7 @@ export const RenderSingleTask = ({task_id}) => {
|
||||
}
|
||||
});
|
||||
return (
|
||||
taskData.id &&
|
||||
taskData.id > 0 &&
|
||||
<TaskDisplay me={me} task={taskData} command_id={taskData.command === null ? 0 : taskData.command.id} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {jwtDecode} from 'jwt-decode';
|
||||
import {meState} from './cache';
|
||||
import {getSkewedNow} from "./components/utilities/Time";
|
||||
|
||||
export const mythicUIVersion = "0.3.82";
|
||||
export const mythicUIVersion = "0.3.83";
|
||||
|
||||
let fetchingNewToken = false;
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ html, body, #root {
|
||||
.MuiTableCell-head {
|
||||
background-color: ${(props) => props.theme.tableHeader} !important;
|
||||
font-weight: bold;
|
||||
border-top: 2px solid grey;
|
||||
border-bottom: 2px solid grey;
|
||||
}
|
||||
tr:nth-child(even) {
|
||||
background-color: ${(props) => props.theme.palette.mode === 'dark' ? props.theme.tableHover + "0D" : props.theme.tableHover + "80"};
|
||||
@@ -181,15 +183,7 @@ tspan {
|
||||
}
|
||||
|
||||
.selectedTask {
|
||||
padding: 3px 2px 6px 3px;
|
||||
background:
|
||||
linear-gradient(90deg, ${(props) => props.theme.palette.secondary.main} 50%, transparent 0) repeat-x,
|
||||
linear-gradient(90deg, ${(props) => props.theme.palette.secondary.main} 50%, transparent 0) repeat-x,
|
||||
linear-gradient(0deg, ${(props) => props.theme.palette.secondary.main} 50%, transparent 0) repeat-y,
|
||||
linear-gradient(0deg, ${(props) => props.theme.palette.secondary.main} 50%, transparent 0) repeat-y,
|
||||
${(props) => props.theme.palette.background.default + "CC"} !important;
|
||||
background-size: 8px 3px, 8px 3px, 3px 8px, 3px 8px !important;
|
||||
background-position: 0 0, 0 100%, 0 0, 100% 0 !important; // top bottom left right
|
||||
background-color: ${(props) => props.theme.selectedCallbackColor + "DD"} !important;
|
||||
}
|
||||
*::-webkit-scrollbar {
|
||||
width: 0.4em;
|
||||
@@ -217,9 +211,11 @@ tspan {
|
||||
box-sizing: border-box;
|
||||
justify-content: space-between;
|
||||
user-select: none;
|
||||
border-top: 2px solid grey;
|
||||
border-bottom: 2px solid grey;
|
||||
background-color: ${(props) => props.theme.tableHeader} !important;
|
||||
&:first-child-of-type {
|
||||
border-left: 1px solid grey;
|
||||
border-left: 2px solid grey;
|
||||
}
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.tableHover};
|
||||
|
||||
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.3.21] - 2025-12-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Added support for `COMPOSE_FILE` in the `.env` to allow dynamically composing multiple docker compose files together
|
||||
- Fixed a check for docker errors when PTY is available
|
||||
|
||||
## [0.3.20] - 2025-10-28
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -3,8 +3,6 @@ package config
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"github.com/MythicMeta/Mythic_CLI/cmd/utils"
|
||||
"github.com/spf13/viper"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -12,6 +10,9 @@ import (
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/MythicMeta/Mythic_CLI/cmd/utils"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var MythicPossibleServices = []string{
|
||||
@@ -366,6 +367,10 @@ If you need to rebuild a specific container, you should use './mythic-cli build
|
||||
This will also delete any volumes in use (which will remove things like C2 Profile's config.json updates).
|
||||
To keep these around when starting or building, use the --keep-volume flag`
|
||||
|
||||
mythicEnv.SetDefault("COMPOSE_FILE", "docker-compose.yml")
|
||||
mythicEnvInfo["COMPOSE_FILE"] = `This is a specific Docker Compose variable you can use to configure docker compose overrides. You specify multiple files separated by : (colon) and they're squashed together for what's actually executed instead of just Mythic's default docker-compose file.
|
||||
More info can be found here: https://docs.docker.com/compose/how-tos/environment-variables/envvars/#compose_file.`
|
||||
|
||||
// Mythic instance configuration ---------------------------------------------
|
||||
mythicEnv.SetDefault("mythic_admin_user", "mythic_admin")
|
||||
mythicEnvInfo["mythic_admin_user"] = `This configures the name of the first user in Mythic when Mythic starts for the first time.
|
||||
|
||||
@@ -4,5 +4,5 @@ package config
|
||||
|
||||
var (
|
||||
// Version Mythic CLI version
|
||||
Version = "v0.3.20"
|
||||
Version = "v0.3.21"
|
||||
)
|
||||
|
||||
@@ -2,12 +2,13 @@ package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/MythicMeta/Mythic_CLI/cmd/config"
|
||||
"github.com/MythicMeta/Mythic_CLI/cmd/manager"
|
||||
"github.com/MythicMeta/Mythic_CLI/cmd/utils"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/MythicMeta/Mythic_CLI/cmd/config"
|
||||
"github.com/MythicMeta/Mythic_CLI/cmd/manager"
|
||||
"github.com/MythicMeta/Mythic_CLI/cmd/utils"
|
||||
)
|
||||
|
||||
func AddMythicService(service string, removeVolume bool) {
|
||||
@@ -451,6 +452,7 @@ func AddMythicService(service string, removeVolume bool) {
|
||||
"./MythicReactUI/public:/app/public",
|
||||
"./MythicReactUI/package.json:/app/package.json",
|
||||
"./MythicReactUI/package-lock.json:/app/package-lock.json",
|
||||
"./MythicReactUI/config-overrides.js:/app/config-overrides.js",
|
||||
"./mythic-react-docker/mythic/public:/app/build",
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -6,6 +6,19 @@ import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/MythicMeta/Mythic_CLI/cmd/config"
|
||||
"github.com/MythicMeta/Mythic_CLI/cmd/utils"
|
||||
"github.com/creack/pty"
|
||||
@@ -19,18 +32,6 @@ import (
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/mod/semver"
|
||||
"gopkg.in/yaml.v3"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DockerComposeManager struct {
|
||||
@@ -1369,14 +1370,13 @@ func (d *DockerComposeManager) runDockerCompose(args []string) error {
|
||||
lookPath, err := exec.LookPath("docker")
|
||||
if err != nil {
|
||||
log.Fatalf("[-] docker is not installed or available in the current PATH\n")
|
||||
} else {
|
||||
// adjust the current args for docker compose subcommand
|
||||
args = append([]string{"compose"}, args...)
|
||||
}
|
||||
// adjust the current args for docker compose subcommand
|
||||
args = append([]string{"compose"}, args...)
|
||||
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
log.Fatalf("[-] Failed to get lookPath to current executable\n")
|
||||
log.Fatalf("[-] Failed to get lookPath to current executable\n%v", err)
|
||||
}
|
||||
exePath := filepath.Dir(exe)
|
||||
command := exec.Command(lookPath, args...)
|
||||
@@ -1386,11 +1386,11 @@ func (d *DockerComposeManager) runDockerCompose(args []string) error {
|
||||
if err != nil {
|
||||
stdout, err := command.StdoutPipe()
|
||||
if err != nil {
|
||||
log.Fatalf("[-] Failed to get stdout pipe for running docker-compose\n")
|
||||
log.Fatalf("[-] Failed to get stdout pipe for running docker-compose\n%v", err)
|
||||
}
|
||||
stderr, err := command.StderrPipe()
|
||||
if err != nil {
|
||||
log.Fatalf("[-] Failed to get stderr pipe for running docker-compose\n")
|
||||
log.Fatalf("[-] Failed to get stderr pipe for running docker-compose\n%v", err)
|
||||
}
|
||||
|
||||
stdoutScanner := bufio.NewScanner(stdout)
|
||||
@@ -1415,10 +1415,20 @@ func (d *DockerComposeManager) runDockerCompose(args []string) error {
|
||||
log.Printf("[*] Docker compose command: %v\n", args)
|
||||
return err
|
||||
}
|
||||
if command.ProcessState.ExitCode() != 0 {
|
||||
log.Fatalf("[-] Error from docker-compose: %v\n", command.ProcessState.ExitCode())
|
||||
}
|
||||
} else {
|
||||
io.Copy(os.Stdout, f)
|
||||
processState, err := command.Process.Wait()
|
||||
if err != nil {
|
||||
log.Printf("[-] Error from docker-compose: %v\n", err)
|
||||
return err
|
||||
}
|
||||
if processState.ExitCode() != 0 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func (d *DockerComposeManager) setDockerComposeDefaultsAndWrite(curConfig map[string]interface{}) error {
|
||||
|
||||
@@ -210,6 +210,14 @@ type Mutation {
|
||||
): createc2instanceOutput
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
custombrowserExportFunction(
|
||||
tree_type: String!
|
||||
host: String!
|
||||
path: String!
|
||||
): custombrowserExportFunctionOutput
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
deleteAPIToken(
|
||||
apitokens_id: Int!
|
||||
@@ -1268,3 +1276,8 @@ type dynamicQueryBuildParameterOutput {
|
||||
choices: [String]
|
||||
}
|
||||
|
||||
type custombrowserExportFunctionOutput {
|
||||
status: String!
|
||||
error: String
|
||||
}
|
||||
|
||||
|
||||
@@ -291,6 +291,16 @@ actions:
|
||||
- role: mythic_admin
|
||||
- role: developer
|
||||
comment: Creates a new saved instance of a C2 Profile with a given name
|
||||
- name: custombrowserExportFunction
|
||||
definition:
|
||||
kind: synchronous
|
||||
handler: '{{MYTHIC_ACTIONS_URL_BASE}}/custombrowser_export_function_webhook'
|
||||
forward_client_headers: true
|
||||
permissions:
|
||||
- role: operator
|
||||
- role: operation_admin
|
||||
- role: mythic_admin
|
||||
- role: developer
|
||||
- name: deleteAPIToken
|
||||
definition:
|
||||
kind: synchronous
|
||||
@@ -1000,4 +1010,5 @@ custom_types:
|
||||
- name: updatePayloadOutput
|
||||
- name: eventingTriggerManualBulkOutput
|
||||
- name: dynamicQueryBuildParameterOutput
|
||||
- name: custombrowserExportFunctionOutput
|
||||
scalars: []
|
||||
|
||||
@@ -2662,6 +2662,7 @@
|
||||
- name
|
||||
- parameter_group_name
|
||||
- type
|
||||
- verifier_regex
|
||||
filter: {}
|
||||
- role: mythic_admin
|
||||
permission:
|
||||
@@ -2685,6 +2686,7 @@
|
||||
- name
|
||||
- parameter_group_name
|
||||
- type
|
||||
- verifier_regex
|
||||
filter: {}
|
||||
- role: operation_admin
|
||||
permission:
|
||||
@@ -2708,6 +2710,7 @@
|
||||
- name
|
||||
- parameter_group_name
|
||||
- type
|
||||
- verifier_regex
|
||||
filter: {}
|
||||
- role: operator
|
||||
permission:
|
||||
@@ -2731,6 +2734,7 @@
|
||||
- name
|
||||
- parameter_group_name
|
||||
- type
|
||||
- verifier_regex
|
||||
filter: {}
|
||||
- role: spectator
|
||||
permission:
|
||||
@@ -2754,6 +2758,7 @@
|
||||
- name
|
||||
- parameter_group_name
|
||||
- type
|
||||
- verifier_regex
|
||||
filter: {}
|
||||
- table:
|
||||
name: consuming_container
|
||||
@@ -3089,6 +3094,137 @@
|
||||
_eq: X-Hasura-current-operation-id
|
||||
set:
|
||||
operator_id: x-hasura-user-id
|
||||
- table:
|
||||
name: custombrowser
|
||||
schema: public
|
||||
select_permissions:
|
||||
- role: developer
|
||||
permission:
|
||||
columns:
|
||||
- container_running
|
||||
- deleted
|
||||
- indicate_partial_listing
|
||||
- show_current_path
|
||||
- id
|
||||
- columns
|
||||
- default_visible_columns
|
||||
- extra_table_inputs
|
||||
- row_actions
|
||||
- author
|
||||
- description
|
||||
- export_function
|
||||
- name
|
||||
- semver
|
||||
- separator
|
||||
- type
|
||||
filter: {}
|
||||
comment: ""
|
||||
- role: mythic_admin
|
||||
permission:
|
||||
columns:
|
||||
- container_running
|
||||
- deleted
|
||||
- indicate_partial_listing
|
||||
- show_current_path
|
||||
- id
|
||||
- columns
|
||||
- default_visible_columns
|
||||
- extra_table_inputs
|
||||
- row_actions
|
||||
- author
|
||||
- description
|
||||
- export_function
|
||||
- name
|
||||
- semver
|
||||
- separator
|
||||
- type
|
||||
filter: {}
|
||||
comment: ""
|
||||
- role: operation_admin
|
||||
permission:
|
||||
columns:
|
||||
- container_running
|
||||
- deleted
|
||||
- indicate_partial_listing
|
||||
- show_current_path
|
||||
- id
|
||||
- columns
|
||||
- default_visible_columns
|
||||
- extra_table_inputs
|
||||
- row_actions
|
||||
- author
|
||||
- description
|
||||
- export_function
|
||||
- name
|
||||
- semver
|
||||
- separator
|
||||
- type
|
||||
filter: {}
|
||||
comment: ""
|
||||
- role: operator
|
||||
permission:
|
||||
columns:
|
||||
- container_running
|
||||
- deleted
|
||||
- indicate_partial_listing
|
||||
- show_current_path
|
||||
- id
|
||||
- columns
|
||||
- default_visible_columns
|
||||
- extra_table_inputs
|
||||
- row_actions
|
||||
- author
|
||||
- description
|
||||
- export_function
|
||||
- name
|
||||
- semver
|
||||
- separator
|
||||
- type
|
||||
filter: {}
|
||||
comment: ""
|
||||
- role: spectator
|
||||
permission:
|
||||
columns:
|
||||
- container_running
|
||||
- deleted
|
||||
- indicate_partial_listing
|
||||
- show_current_path
|
||||
- id
|
||||
- columns
|
||||
- default_visible_columns
|
||||
- extra_table_inputs
|
||||
- row_actions
|
||||
- author
|
||||
- description
|
||||
- export_function
|
||||
- name
|
||||
- semver
|
||||
- separator
|
||||
- type
|
||||
filter: {}
|
||||
comment: ""
|
||||
update_permissions:
|
||||
- role: mythic_admin
|
||||
permission:
|
||||
columns:
|
||||
- deleted
|
||||
filter: {}
|
||||
check: null
|
||||
comment: ""
|
||||
- role: operation_admin
|
||||
permission:
|
||||
columns:
|
||||
- deleted
|
||||
filter: {}
|
||||
check: null
|
||||
comment: ""
|
||||
- role: operator
|
||||
permission:
|
||||
columns:
|
||||
- deleted
|
||||
filter: {}
|
||||
check: null
|
||||
comment: ""
|
||||
- table:
|
||||
name: disabledcommandsprofile
|
||||
schema: public
|
||||
@@ -4892,6 +5028,11 @@
|
||||
name: tag
|
||||
schema: public
|
||||
computed_fields:
|
||||
- name: display_path_text
|
||||
definition:
|
||||
function:
|
||||
name: mythictree_display_path
|
||||
schema: public
|
||||
- name: full_path_text
|
||||
definition:
|
||||
function:
|
||||
@@ -4913,7 +5054,9 @@
|
||||
columns:
|
||||
- can_have_children
|
||||
- deleted
|
||||
- has_children
|
||||
- success
|
||||
- display_path
|
||||
- full_path
|
||||
- name
|
||||
- parent_path
|
||||
@@ -4929,6 +5072,7 @@
|
||||
- tree_type
|
||||
- timestamp
|
||||
computed_fields:
|
||||
- display_path_text
|
||||
- full_path_text
|
||||
- name_text
|
||||
- parent_path_text
|
||||
@@ -4941,7 +5085,9 @@
|
||||
columns:
|
||||
- can_have_children
|
||||
- deleted
|
||||
- has_children
|
||||
- success
|
||||
- display_path
|
||||
- full_path
|
||||
- name
|
||||
- parent_path
|
||||
@@ -4957,6 +5103,7 @@
|
||||
- tree_type
|
||||
- timestamp
|
||||
computed_fields:
|
||||
- display_path_text
|
||||
- full_path_text
|
||||
- name_text
|
||||
- parent_path_text
|
||||
@@ -4969,7 +5116,9 @@
|
||||
columns:
|
||||
- can_have_children
|
||||
- deleted
|
||||
- has_children
|
||||
- success
|
||||
- display_path
|
||||
- full_path
|
||||
- name
|
||||
- parent_path
|
||||
@@ -4985,6 +5134,7 @@
|
||||
- tree_type
|
||||
- timestamp
|
||||
computed_fields:
|
||||
- display_path_text
|
||||
- full_path_text
|
||||
- name_text
|
||||
- parent_path_text
|
||||
@@ -4997,7 +5147,9 @@
|
||||
columns:
|
||||
- can_have_children
|
||||
- deleted
|
||||
- has_children
|
||||
- success
|
||||
- display_path
|
||||
- full_path
|
||||
- name
|
||||
- parent_path
|
||||
@@ -5013,6 +5165,7 @@
|
||||
- tree_type
|
||||
- timestamp
|
||||
computed_fields:
|
||||
- display_path_text
|
||||
- full_path_text
|
||||
- name_text
|
||||
- parent_path_text
|
||||
@@ -5025,7 +5178,9 @@
|
||||
columns:
|
||||
- can_have_children
|
||||
- deleted
|
||||
- has_children
|
||||
- success
|
||||
- display_path
|
||||
- full_path
|
||||
- name
|
||||
- parent_path
|
||||
@@ -5041,6 +5196,7 @@
|
||||
- tree_type
|
||||
- timestamp
|
||||
computed_fields:
|
||||
- display_path_text
|
||||
- full_path_text
|
||||
- name_text
|
||||
- parent_path_text
|
||||
@@ -6155,6 +6311,7 @@
|
||||
- build_stdout
|
||||
- description
|
||||
- os
|
||||
- payload_type_semver
|
||||
- uuid
|
||||
- creation_time
|
||||
- timestamp
|
||||
@@ -6185,6 +6342,7 @@
|
||||
- build_stdout
|
||||
- description
|
||||
- os
|
||||
- payload_type_semver
|
||||
- uuid
|
||||
- creation_time
|
||||
- timestamp
|
||||
@@ -6215,6 +6373,7 @@
|
||||
- build_stdout
|
||||
- description
|
||||
- os
|
||||
- payload_type_semver
|
||||
- uuid
|
||||
- creation_time
|
||||
- timestamp
|
||||
@@ -6245,6 +6404,7 @@
|
||||
- build_stdout
|
||||
- description
|
||||
- os
|
||||
- payload_type_semver
|
||||
- uuid
|
||||
- creation_time
|
||||
- timestamp
|
||||
@@ -6275,6 +6435,7 @@
|
||||
- build_stdout
|
||||
- description
|
||||
- os
|
||||
- payload_type_semver
|
||||
- uuid
|
||||
- creation_time
|
||||
- timestamp
|
||||
@@ -6854,8 +7015,10 @@
|
||||
- c2_parameter_deviations
|
||||
- supported_c2
|
||||
- supported_os
|
||||
- supported_wrapping
|
||||
- agent_type
|
||||
- author
|
||||
- command_help_function
|
||||
- file_extension
|
||||
- message_format
|
||||
- name
|
||||
@@ -6881,8 +7044,10 @@
|
||||
- c2_parameter_deviations
|
||||
- supported_c2
|
||||
- supported_os
|
||||
- supported_wrapping
|
||||
- agent_type
|
||||
- author
|
||||
- command_help_function
|
||||
- file_extension
|
||||
- message_format
|
||||
- name
|
||||
@@ -6908,8 +7073,10 @@
|
||||
- c2_parameter_deviations
|
||||
- supported_c2
|
||||
- supported_os
|
||||
- supported_wrapping
|
||||
- agent_type
|
||||
- author
|
||||
- command_help_function
|
||||
- file_extension
|
||||
- message_format
|
||||
- name
|
||||
@@ -6935,8 +7102,10 @@
|
||||
- c2_parameter_deviations
|
||||
- supported_c2
|
||||
- supported_os
|
||||
- supported_wrapping
|
||||
- agent_type
|
||||
- author
|
||||
- command_help_function
|
||||
- file_extension
|
||||
- message_format
|
||||
- name
|
||||
@@ -6962,8 +7131,10 @@
|
||||
- c2_parameter_deviations
|
||||
- supported_c2
|
||||
- supported_os
|
||||
- supported_wrapping
|
||||
- agent_type
|
||||
- author
|
||||
- command_help_function
|
||||
- file_extension
|
||||
- message_format
|
||||
- name
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
var DB *sqlx.DB
|
||||
var currentMigrationVersion int64 = 3003014
|
||||
var currentMigrationVersion int64 = 3003015
|
||||
|
||||
// initial structs made with './tables-to-go -u mythic_user -p [password here] -h [ip here] -v -d mythic_db -of output -pn database_structs'
|
||||
// package pulled from https://github.com/fraenky8/tables-to-go
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
-- +migrate Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
create sequence IF NOT EXISTS "public"."custombrowser_id_seq";
|
||||
create table IF NOT EXISTS "public"."custombrowser" (
|
||||
"id" integer not null default nextval('custombrowser_id_seq'::regclass),
|
||||
"name" text not null,
|
||||
"type" text not null default 'file'::text,
|
||||
"separator" text not null default '/'::text,
|
||||
"deleted" boolean not null default false,
|
||||
"columns" jsonb not null default jsonb_build_array(),
|
||||
"default_visible_columns" jsonb not null default jsonb_build_array(),
|
||||
"export_function" text not null default ''::text,
|
||||
"indicate_partial_listing" boolean not null default true,
|
||||
"show_current_path" boolean not null default true,
|
||||
"row_actions" jsonb not null default jsonb_build_array(),
|
||||
"extra_table_inputs" jsonb not null default jsonb_build_array(),
|
||||
"author" text not null default ''::text,
|
||||
"description" text not null default ''::text,
|
||||
"container_running" boolean not null default false,
|
||||
"semver" text not null default ''::text
|
||||
);
|
||||
|
||||
alter table "public"."commandparameters" add column IF NOT EXISTS "verifier_regex" text not null default ''::text;
|
||||
alter table "public"."mythictree" add column IF NOT EXISTS "display_path" bytea not null default '\x'::bytea;
|
||||
alter table "public"."mythictree" add column IF NOT EXISTS "has_children" boolean not null default false;
|
||||
alter table "public"."payload" add column IF NOT EXISTS "payload_type_semver" text not null default ''::text;
|
||||
alter table "public"."payloadtype" add column IF NOT EXISTS "supported_wrapping" jsonb not null default jsonb_build_array();
|
||||
alter table "public"."token" alter column "token_id" set data type bigint using "token_id"::bigint;
|
||||
|
||||
alter sequence "public"."custombrowser_id_seq" owned by "public"."custombrowser"."id";
|
||||
alter table "public"."custombrowser" drop constraint if exists "custombrowser_pkey";
|
||||
alter table "public"."custombrowser" drop constraint if exists "custombrowser_name_key";
|
||||
CREATE UNIQUE INDEX custombrowser_name_key ON public.custombrowser USING btree (name);
|
||||
CREATE UNIQUE INDEX custombrowser_pkey ON public.custombrowser USING btree (id);
|
||||
alter table "public"."custombrowser" add constraint "custombrowser_pkey" PRIMARY KEY using index "custombrowser_pkey";
|
||||
alter table "public"."custombrowser" add constraint "custombrowser_name_key" UNIQUE using index "custombrowser_name_key";
|
||||
-- +migrate StatementBegin
|
||||
CREATE OR REPLACE FUNCTION public.mythictree_display_path(fileobj_row mythictree)
|
||||
RETURNS text
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
AS $function$
|
||||
SELECT convert_from(fileobj_row.display_path, 'utf8')
|
||||
$function$
|
||||
;
|
||||
-- +migrate StatementEnd
|
||||
-- +migrate Down
|
||||
-- SQL in section 'Down' is executed when this migration is rolled back
|
||||
|
||||
|
||||
@@ -5,12 +5,13 @@ import (
|
||||
)
|
||||
|
||||
type Callbacktoken struct {
|
||||
ID int `db:"id"`
|
||||
TokenID int `db:"token_id"`
|
||||
CallbackID int `db:"callback_id"`
|
||||
Os string `db:"os"`
|
||||
TaskID int `db:"task_id"`
|
||||
TimestampCreated time.Time `db:"timestamp_created"`
|
||||
Deleted bool `db:"deleted"`
|
||||
Host string `db:"host"`
|
||||
ID int `db:"id" json:"id"`
|
||||
TokenID int `db:"token_id" json:"token_id" `
|
||||
Token Token `db:"token" json:"token"`
|
||||
CallbackID int `db:"callback_id" json:"callback_id"`
|
||||
Os string `db:"os" json:"-"`
|
||||
TaskID int `db:"task_id" json:"task_id"`
|
||||
TimestampCreated time.Time `db:"timestamp_created" json:"timestamp_created"`
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
Host string `db:"host" json:"host"`
|
||||
}
|
||||
|
||||
@@ -21,4 +21,5 @@ type Commandparameters struct {
|
||||
Required bool `db:"required"`
|
||||
UiPosition int `db:"ui_position"`
|
||||
LimitCredentialsByType MythicJSONArray `db:"limit_credentials_by_type"`
|
||||
VerifierRegex string `db:"verifier_regex"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package databaseStructs
|
||||
|
||||
// CustomBrowser is custombrowser table
|
||||
type CustomBrowser struct {
|
||||
ID int `db:"id"`
|
||||
Name string `db:"name"`
|
||||
Description string `db:"description"`
|
||||
ContainerRunning bool `db:"container_running"`
|
||||
Author string `db:"author"`
|
||||
Deleted bool `db:"deleted"`
|
||||
SemVer string `db:"semver"`
|
||||
Type string `db:"type"`
|
||||
Separator string `db:"separator"`
|
||||
Columns MythicJSONArray `db:"columns"`
|
||||
DefaultVisibleColumns MythicJSONArray `db:"default_visible_columns"`
|
||||
ExportFunction string `db:"export_function"`
|
||||
IndicatePartialListing bool `db:"indicate_partial_listing"`
|
||||
ShowCurrentPath bool `db:"show_current_path"`
|
||||
RowActions MythicJSONArray `db:"row_actions"`
|
||||
ExtraTableInputs MythicJSONArray `db:"extra_table_inputs"`
|
||||
}
|
||||
@@ -2,8 +2,9 @@ package databaseStructs
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/its-a-feature/Mythic/utils/structs"
|
||||
"time"
|
||||
|
||||
"github.com/its-a-feature/Mythic/utils/structs"
|
||||
)
|
||||
|
||||
type MythicTree struct {
|
||||
@@ -19,6 +20,8 @@ type MythicTree struct {
|
||||
FullPath []byte `db:"full_path" mapstructure:"full_path"`
|
||||
Comment string `db:"comment" mapstructure:"comment"`
|
||||
CanHaveChildren bool `db:"can_have_children" mapstructure:"can_have_children"`
|
||||
HasChildren bool `db:"has_children" mapstructure:"has_children"`
|
||||
DisplayPath []byte `db:"display_path" mapstructure:"display_path"`
|
||||
Success sql.NullBool `db:"success"`
|
||||
Deleted bool `db:"deleted" mapstructure:"deleted"`
|
||||
Metadata MythicJSONText `db:"metadata"`
|
||||
|
||||
@@ -34,4 +34,5 @@ type Payload struct {
|
||||
EventStepInstanceID structs.NullInt64 `db:"eventstepinstance_id" json:"event_step_instance_id" mapstructure:"event_step_instance_id"`
|
||||
APITokensID structs.NullInt64 `db:"apitokens_id" json:"api_tokens_id" mapstructure:"apitokens_id"`
|
||||
CallbackAllowed bool `db:"callback_allowed" json:"callback_allowed" mapstructure:"callback_allowed"`
|
||||
PayloadTypeSemver string `db:"payload_type_semver" json:"payload_type_semver" mapstructure:"payload_type_semver"`
|
||||
}
|
||||
|
||||
@@ -33,4 +33,5 @@ type Payloadtype struct {
|
||||
C2ParameterDeviations MythicJSONText `db:"c2_parameter_deviations"`
|
||||
SupportedC2 MythicJSONArray `db:"supported_c2"`
|
||||
CommandHelpFunction string `db:"command_help_function"`
|
||||
SupportedWrapping MythicJSONArray `db:"supported_wrapping"`
|
||||
}
|
||||
|
||||
@@ -4,27 +4,27 @@ import "time"
|
||||
|
||||
type Token struct {
|
||||
// mythic supplied
|
||||
ID int `db:"id"`
|
||||
TaskID int `db:"task_id"`
|
||||
Deleted bool `db:"deleted"`
|
||||
Host string `db:"host"`
|
||||
Description string `db:"description"`
|
||||
OperationID int `db:"operation_id"`
|
||||
Timestamp time.Time `db:"timestamp"`
|
||||
ID int `db:"id" json:"id"`
|
||||
TaskID int `db:"task_id" json:"task_id"`
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
Host string `db:"host" json:"host"`
|
||||
Description *string `db:"description" json:"description"`
|
||||
OperationID int `db:"operation_id" json:"operation_id"`
|
||||
Timestamp time.Time `db:"timestamp" json:"timestamp"`
|
||||
// agent supplied
|
||||
TokenID uint64 `db:"token_id"`
|
||||
User string `db:"user"`
|
||||
Groups string `db:"groups"`
|
||||
Privileges string `db:"privileges"`
|
||||
ThreadID int `db:"thread_id"`
|
||||
ProcessID int `db:"process_id"`
|
||||
SessionID int `db:"session_id"`
|
||||
LogonSID string `db:"logon_sid"`
|
||||
IntegrityLevelSID string `db:"integrity_level_sid"`
|
||||
AppContainerSID string `db:"app_container_sid"`
|
||||
AppContainerNumber int `db:"app_container_number"`
|
||||
DefaultDacl string `db:"default_dacl"`
|
||||
Restricted bool `db:"restricted"`
|
||||
Handle int `db:"handle"`
|
||||
Capabilities string `db:"capabilities"`
|
||||
TokenID int64 `db:"token_id" json:"token_id"`
|
||||
User string `db:"user" json:"user"`
|
||||
Groups string `db:"groups" json:"groups"`
|
||||
Privileges string `db:"privileges" json:"privileges"`
|
||||
ThreadID int `db:"thread_id" json:"thread_id"`
|
||||
ProcessID int `db:"process_id" json:"process_id"`
|
||||
SessionID int `db:"session_id" json:"session_id"`
|
||||
LogonSID string `db:"logon_sid" json:"logon_sid"`
|
||||
IntegrityLevelSID string `db:"integrity_level_sid" json:"integrity_level_sid"`
|
||||
AppContainerSID string `db:"app_container_sid" json:"app_container_sid"`
|
||||
AppContainerNumber int `db:"app_container_number" json:"app_container_number"`
|
||||
DefaultDacl string `db:"default_dacl" json:"default_dacl"`
|
||||
Restricted bool `db:"restricted" json:"restricted"`
|
||||
Handle int `db:"handle" json:"handle"`
|
||||
Capabilities string `db:"capabilities" json:"capabilities"`
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package database
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
mythicCrypto "github.com/its-a-feature/Mythic/crypto"
|
||||
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
|
||||
"github.com/its-a-feature/Mythic/logging"
|
||||
@@ -179,14 +180,15 @@ func ResolveAllOperationsMessage(message string, operationID int) {
|
||||
updateObject := databaseStructs.Operationeventlog{
|
||||
Message: message,
|
||||
OperationID: operationID,
|
||||
Level: MESSAGE_LEVEL_INFO,
|
||||
}
|
||||
if operationID == 0 {
|
||||
updateObject.OperationID = operation.ID
|
||||
}
|
||||
if _, err := DB.NamedExec(`UPDATE operationeventlog SET
|
||||
resolved=true
|
||||
WHERE level='warning' AND resolved=false AND deleted=false AND message=:message AND operation_id=:operation_id`, updateObject); err != nil {
|
||||
logging.LogError(err, "Failed to increase count on operationeventlog")
|
||||
WHERE warning=true AND resolved=false AND deleted=false AND message=:message AND operation_id=:operation_id`, updateObject); err != nil {
|
||||
logging.LogError(err, "Failed to resolve message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,14 @@ import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/its-a-feature/Mythic/authentication/mythicjwt"
|
||||
"github.com/its-a-feature/Mythic/grpc"
|
||||
"github.com/its-a-feature/Mythic/utils"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/its-a-feature/Mythic/authentication/mythicjwt"
|
||||
"github.com/its-a-feature/Mythic/grpc"
|
||||
"github.com/its-a-feature/Mythic/utils"
|
||||
|
||||
"github.com/its-a-feature/Mythic/database"
|
||||
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
|
||||
"github.com/its-a-feature/Mythic/logging"
|
||||
@@ -24,6 +25,8 @@ var checkContainerStatusAddTrChannel = make(chan databaseStructs.Translationcont
|
||||
var translationContainersToCheck = map[string]databaseStructs.Translationcontainer{}
|
||||
var checkContainerStatusAddConsumingContainerChannel = make(chan databaseStructs.ConsumingContainer)
|
||||
var consumingContainersToCheck = map[string]databaseStructs.ConsumingContainer{}
|
||||
var checkContainerStatusAddCustomBrowserChannel = make(chan databaseStructs.CustomBrowser)
|
||||
var customBrowsersToCheck = map[string]databaseStructs.CustomBrowser{}
|
||||
|
||||
func checkContainerStatusAddPT() {
|
||||
for {
|
||||
@@ -49,6 +52,12 @@ func checkContainerStatusAddConsumingContainer() {
|
||||
consumingContainersToCheck[cc.Name] = cc
|
||||
}
|
||||
}
|
||||
func checkContainerStatusAddCustomBrowser() {
|
||||
for {
|
||||
cc := <-checkContainerStatusAddCustomBrowserChannel
|
||||
customBrowsersToCheck[cc.Name] = cc
|
||||
}
|
||||
}
|
||||
func initializeContainers() {
|
||||
payloadtypes := []databaseStructs.Payloadtype{}
|
||||
if err := database.DB.Select(&payloadtypes, `SELECT * FROM payloadtype`); err != nil {
|
||||
@@ -82,6 +91,14 @@ func initializeContainers() {
|
||||
checkContainerStatusAddConsumingContainerChannel <- consumingContainers[i]
|
||||
}
|
||||
}
|
||||
customBrowsers := []databaseStructs.CustomBrowser{}
|
||||
if err := database.DB.Select(&customBrowsers, `SELECT * FROM custombrowser`); err != nil {
|
||||
logging.LogError(err, "Failed to fetch consuming containers")
|
||||
} else {
|
||||
for i, _ := range customBrowsers {
|
||||
checkContainerStatusAddCustomBrowserChannel <- customBrowsers[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tr = &http.Transport{
|
||||
@@ -193,6 +210,8 @@ func CreateGraphQLSpectatorAPITokenAndSendOnStartMessage(containerName string) {
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to send container on start")
|
||||
}
|
||||
// wait a few seconds between each message to give the container a chance to process the message
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}
|
||||
func updateAPITokenAfter5Minutes(apitoken_id int) {
|
||||
@@ -213,6 +232,7 @@ func checkContainerStatus() {
|
||||
go checkContainerStatusAddC2()
|
||||
go checkContainerStatusAddTR()
|
||||
go checkContainerStatusAddConsumingContainer()
|
||||
go checkContainerStatusAddCustomBrowser()
|
||||
go initializeContainers()
|
||||
for {
|
||||
time.Sleep(CHECK_CONTAINER_STATUS_DELAY)
|
||||
@@ -382,6 +402,37 @@ func checkContainerStatus() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for container := range customBrowsersToCheck {
|
||||
// check that a container is online
|
||||
//logging.LogDebug("checking container", "container", container)
|
||||
running := utils.SliceContains(existingQueues, GetCustomBrowserExportFunctionRoutingKey(consumingContainersToCheck[container].Name))
|
||||
//logging.LogInfo("checking container running", "container", container, "running", running, "current_running", c2profilesToCheck[container].ContainerRunning)
|
||||
if running != consumingContainersToCheck[container].ContainerRunning {
|
||||
if entry, ok := consumingContainersToCheck[container]; ok {
|
||||
entry.ContainerRunning = running
|
||||
_, err = database.DB.NamedExec(`UPDATE custombrowser SET
|
||||
container_running=:container_running, deleted=false
|
||||
WHERE id=:id`, entry,
|
||||
)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to set container running status", "container_running", consumingContainersToCheck[container].ContainerRunning, "container", container)
|
||||
continue
|
||||
}
|
||||
consumingContainersToCheck[container] = entry
|
||||
if !running {
|
||||
SendAllOperationsMessage(
|
||||
getDownContainerMessage(container),
|
||||
0, fmt.Sprintf("%s_container_down", container), database.MESSAGE_LEVEL_INFO, true)
|
||||
} else {
|
||||
go database.ResolveAllOperationsMessage(getDownContainerMessage(container), 0)
|
||||
go CreateGraphQLSpectatorAPITokenAndSendOnStartMessage(container)
|
||||
}
|
||||
} else {
|
||||
logging.LogError(nil, "Failed to get custom browser from map for updating running status")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const (
|
||||
CHECK_CONTAINER_STATUS_DELAY = 10 * time.Second
|
||||
TIME_FORMAT_STRING_YYYY_MM_DD = "2006-01-02"
|
||||
TIME_FORMAT_STRING_YYYY_MM_DD_HH_MM_SS = "2006-01-02 15:04:05 Z07"
|
||||
RPC_TIMEOUT = 5 * time.Second
|
||||
RPC_TIMEOUT = 10 * time.Second
|
||||
TASK_STATUS_CONTAINER_DOWN = "Error: Container Down"
|
||||
)
|
||||
|
||||
@@ -76,6 +76,11 @@ const (
|
||||
|
||||
CONTAINER_ON_START = "container_on_start"
|
||||
CONTAINER_ON_START_RESPONSE = "container_on_start_response"
|
||||
|
||||
// CUSTOM_BROWSER routes
|
||||
CUSTOMBROWSER_SYNC_ROUTING_KEY = "custombrowser_sync"
|
||||
CUSTOMBROWSER_EXPORT_FUNCTION = "custombrowser_exportfunction"
|
||||
CUSTOMBROWSER_EXPORT_FUNCTION_RESPONSE = "custombrowser_exportfunction_response"
|
||||
)
|
||||
|
||||
// Direct fanout rabbitmq routes where the container is consuming messages and responding back to Mythic, but others can also listen in and consume.
|
||||
@@ -223,6 +228,7 @@ const (
|
||||
// MYTHIC_RPC_CALLBACKTOKEN_CREATE callback token operations
|
||||
MYTHIC_RPC_CALLBACKTOKEN_CREATE = "mythic_rpc_callbacktoken_create"
|
||||
MYTHIC_RPC_CALLBACKTOKEN_REMOVE = "mythic_rpc_callbacktoken_remove"
|
||||
MYTHIC_RPC_CALLBACKTOKEN_SEARCH = "mythic_rpc_callbacktoken_search"
|
||||
// MYTHIC_RPC_TOKEN_CREATE token operations
|
||||
MYTHIC_RPC_TOKEN_CREATE = "mythic_rpc_token_create"
|
||||
MYTHIC_RPC_TOKEN_REMOVE = "mythic_rpc_token_remove"
|
||||
@@ -240,6 +246,8 @@ const (
|
||||
MYTHIC_RPC_APITOKEN_CREATE = "mythic_rpc_apitoken_create"
|
||||
// Agent Message
|
||||
MYTHIC_RPC_HANDLE_AGENT_JSON = "mythic_rpc_handle_agent_message_json"
|
||||
// CustomBrowser
|
||||
CUSTOMBROWSER_SEARCH = "mythic_rpc_custombrowser_search"
|
||||
// MYTHIC_RPC_BLANK blank
|
||||
MYTHIC_RPC_BLANK = "mythic_rpc_blank"
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/its-a-feature/Mythic/database"
|
||||
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
|
||||
@@ -124,7 +125,7 @@ func c2Sync(in C2SyncMessage) error {
|
||||
return errors.New("Can't have c2 container with empty name - bad sync")
|
||||
} else if !isValidContainerVersion(in.ContainerVersion) {
|
||||
logging.LogError(nil, "attempting to sync bad c2 container version")
|
||||
return errors.New(fmt.Sprintf("Version, %s, isn't supported. The max supported version is %s. \nThis likely means your PyPi or Golang library is out of date and should be updated.", in.ContainerVersion, validContainerVersionMax))
|
||||
return errors.New(fmt.Sprintf("Version, %s, isn't supported. The max supported version is < %s. \nThis likely means your PyPi or Golang library is out of date and should be updated.", in.ContainerVersion, validContainerVersionMax))
|
||||
}
|
||||
if err := database.DB.Get(&c2Profile, `SELECT * FROM c2profile WHERE "name"=$1`, in.Profile.Name); err != nil {
|
||||
// this means we don't have the c2 profile, so we need to create it and all the associated components
|
||||
@@ -196,7 +197,6 @@ func c2Sync(in C2SyncMessage) error {
|
||||
go reSyncPayloadTypes()
|
||||
}
|
||||
checkContainerStatusAddC2Channel <- c2Profile
|
||||
go CreateGraphQLSpectatorAPITokenAndSendOnStartMessage(c2Profile.Name)
|
||||
absPath, err := filepath.Abs(filepath.Join(".", "static", fmt.Sprintf("%s_light.svg", in.Profile.Name)))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -365,8 +365,10 @@ func autoStartC2Profile(c2Profile databaseStructs.C2profile) *C2StartServerMessa
|
||||
// on a new sync, if it's not p2p, ask it to start
|
||||
var c2StartResp *C2StartServerMessageResponse
|
||||
var err error
|
||||
CreateGraphQLSpectatorAPITokenAndSendOnStartMessage(c2Profile.Name)
|
||||
if !c2Profile.IsP2p {
|
||||
c2StartResp, err = RabbitMQConnection.SendC2RPCStartServer(C2StartServerMessage{Name: c2Profile.Name})
|
||||
time.Sleep(10 * time.Second) // give the server a chance to start back up
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to send start message to C2 profile")
|
||||
c2StartResp.Error = err.Error()
|
||||
@@ -374,14 +376,16 @@ func autoStartC2Profile(c2Profile databaseStructs.C2profile) *C2StartServerMessa
|
||||
UpdateC2ProfileRunningStatus(c2Profile, c2StartResp.InternalServerRunning)
|
||||
if !c2StartResp.InternalServerRunning {
|
||||
go SendAllOperationsMessage(fmt.Sprintf("Failed to start c2 profile %s:\n%s", c2Profile.Name, c2StartResp.Error), 0, "", database.MESSAGE_LEVEL_INFO, true)
|
||||
} else {
|
||||
autoReHostFiles(c2Profile)
|
||||
}
|
||||
}
|
||||
}
|
||||
autoReHostFiles(c2Profile)
|
||||
return c2StartResp
|
||||
}
|
||||
|
||||
func autoReHostFiles(c2Profile databaseStructs.C2profile) {
|
||||
logging.LogInfo("Starting rehost files")
|
||||
fileHostedTagType := databaseStructs.TagType{
|
||||
Name: "FileHosted",
|
||||
}
|
||||
@@ -401,12 +405,16 @@ func autoReHostFiles(c2Profile databaseStructs.C2profile) {
|
||||
for key, _ := range dataStruct {
|
||||
if strings.HasPrefix(key, fmt.Sprintf("%s; ", c2Profile.Name)) {
|
||||
newTagMap := dataStruct[key].(map[string]interface{})
|
||||
logging.LogInfo("sending host file", "c2", newTagMap["c2_profile"].(string), "url", newTagMap["host_url"].(string))
|
||||
c2HostFileResponse, err := RabbitMQConnection.SendC2RPCHostFile(C2HostFileMessage{
|
||||
Name: newTagMap["c2_profile"].(string),
|
||||
FileUUID: newTagMap["agent_file_id"].(string),
|
||||
HostURL: newTagMap["host_url"].(string),
|
||||
Remove: false,
|
||||
})
|
||||
logging.LogInfo("got response from sending host file", "c2", newTagMap["c2_profile"].(string), "url", newTagMap["host_url"].(string))
|
||||
// sleep for 3 seconds to allow the internal c2 binary to start up before we send anything else
|
||||
time.Sleep(10 * time.Second)
|
||||
if err != nil {
|
||||
logging.LogError(err, "failed to send host file message to c2 profile")
|
||||
go SendAllOperationsMessage(fmt.Sprintf(
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/its-a-feature/Mythic/database"
|
||||
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
|
||||
"github.com/its-a-feature/Mythic/eventing"
|
||||
@@ -85,7 +86,7 @@ func consumingServicesSync(in ConsumingContainerSyncMessage) error {
|
||||
return errors.New("can't have consuming container with empty name - bad sync")
|
||||
} else if !isValidContainerVersion(in.ContainerVersion) {
|
||||
logging.LogError(nil, "attempting to sync bad consuming container version")
|
||||
return errors.New(fmt.Sprintf("Version, %s, isn't supported. The max supported version is %s. \nThis likely means your PyPi or Golang library is out of date and should be updated.", in.ContainerVersion, validContainerVersionMax))
|
||||
return errors.New(fmt.Sprintf("Version, %s, isn't supported. The max supported version is < %s. \nThis likely means your PyPi or Golang library is out of date and should be updated.", in.ContainerVersion, validContainerVersionMax))
|
||||
}
|
||||
if err := database.DB.Get(&consumingContainer, `SELECT * FROM consuming_container WHERE "name"=$1`, in.ConsumingContainer.Name); err != nil {
|
||||
// this means we don't have the c2 profile, so we need to create it and all the associated components
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package rabbitmq
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/its-a-feature/Mythic/database"
|
||||
"github.com/its-a-feature/Mythic/logging"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
)
|
||||
|
||||
// TR_SYNC STRUCTS
|
||||
|
||||
func init() {
|
||||
RabbitMQConnection.AddDirectQueue(DirectQueueStruct{
|
||||
Exchange: MYTHIC_EXCHANGE,
|
||||
Queue: CUSTOMBROWSER_EXPORT_FUNCTION_RESPONSE,
|
||||
RoutingKey: CUSTOMBROWSER_EXPORT_FUNCTION_RESPONSE,
|
||||
Handler: processCbExportFunctionResponseMessages,
|
||||
})
|
||||
}
|
||||
|
||||
func processCbExportFunctionResponseMessages(msg amqp.Delivery) {
|
||||
//logging.LogInfo("got message", "routingKey", msg.RoutingKey, "data", msg)
|
||||
response := ExportFunctionMessageResponse{}
|
||||
err := json.Unmarshal(msg.Body, &response)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to process custom browser export response message")
|
||||
return
|
||||
}
|
||||
if !response.Success {
|
||||
go SendAllOperationsMessage(fmt.Sprintf("Failed to export from browser:\n%s", response.Error),
|
||||
response.OperationID, "custombrowser_export_function", database.MESSAGE_LEVEL_INFO, true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package rabbitmq
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/its-a-feature/Mythic/database"
|
||||
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
|
||||
"github.com/its-a-feature/Mythic/logging"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
)
|
||||
|
||||
// CUSTOM_BROWSER_SYNC STRUCTS
|
||||
type CustomBrowserSyncMessageResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type CustomBrowserSyncMessage struct {
|
||||
CustomBrowser CustomBrowser `json:"custombrowser"`
|
||||
ContainerVersion string `json:"container_version"`
|
||||
}
|
||||
|
||||
type CustomBrowserTableColumn struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
FillWidth bool `json:"fillWidth"`
|
||||
Width int64 `json:"width"`
|
||||
DisableSort bool `json:"disableSort"`
|
||||
DisableDoubleClick bool `json:"disableDoubleClick"`
|
||||
DisableFilterMenu bool `json:"disableFilterMenu"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
type CustomBrowserRowAction struct {
|
||||
Name string `json:"name"`
|
||||
UIFeature string `json:"ui_feature"`
|
||||
Icon string `json:"icon"`
|
||||
Color string `json:"color"`
|
||||
SupportsFile bool `json:"supports_file"`
|
||||
SupportsFolder bool `json:"supports_folder"`
|
||||
OpenDialog bool `json:"openDialog"`
|
||||
GetConfirmation bool `json:"getConfirmation"`
|
||||
}
|
||||
type CustomBrowserExtraTableTaskingInput struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Required bool `json:"required"`
|
||||
}
|
||||
type CustomBrowser struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Separator string `json:"separator"`
|
||||
ExportFunction string `json:"export_function"`
|
||||
IndicatePartialListing bool `json:"indicate_partial_listing"`
|
||||
ShowCurrentPath bool `json:"show_current_path"`
|
||||
Description string `json:"description"`
|
||||
Author string `json:"author"`
|
||||
SemVer string `json:"semver"`
|
||||
DefaultVisibleColumns []string `json:"default_visible_columns"`
|
||||
Columns []CustomBrowserTableColumn `json:"columns"`
|
||||
RowActions []CustomBrowserRowAction `json:"row_actions"`
|
||||
ExtraTableInputs []CustomBrowserExtraTableTaskingInput `json:"extra_table_inputs"`
|
||||
}
|
||||
|
||||
var validCustomBrowserTypes = []string{"file"}
|
||||
|
||||
func init() {
|
||||
RabbitMQConnection.AddRPCQueue(RPCQueueStruct{
|
||||
Exchange: MYTHIC_EXCHANGE,
|
||||
Queue: CUSTOMBROWSER_SYNC_ROUTING_KEY,
|
||||
RoutingKey: CUSTOMBROWSER_SYNC_ROUTING_KEY,
|
||||
Handler: processCustomBrowserSyncMessages,
|
||||
})
|
||||
}
|
||||
|
||||
func processCustomBrowserSyncMessages(msg amqp.Delivery) interface{} {
|
||||
//logging.LogInfo("got message", "routingKey", msg.RoutingKey, "data", msg)
|
||||
custombrowserSyncMsg := CustomBrowserSyncMessage{}
|
||||
|
||||
err := json.Unmarshal(msg.Body, &custombrowserSyncMsg)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to process custom browser sync message")
|
||||
go SendAllOperationsMessage(fmt.Sprintf("Failed to sync custom browser profile %s", err.Error()), 0, "", database.MESSAGE_LEVEL_INFO, true)
|
||||
return CustomBrowserSyncMessageResponse{Success: false, Error: err.Error()}
|
||||
}
|
||||
response := CustomBrowserSyncMessageResponse{}
|
||||
err = customBrowserSync(custombrowserSyncMsg)
|
||||
if err != nil {
|
||||
// failed to sync message
|
||||
response.Success = false
|
||||
response.Error = fmt.Sprintf("Error: %v", err)
|
||||
go SendAllOperationsMessage(fmt.Sprintf("Failed to sync %s - %s", custombrowserSyncMsg.CustomBrowser.Name, err.Error()), 0, custombrowserSyncMsg.CustomBrowser.Name, database.MESSAGE_LEVEL_INFO, true)
|
||||
}
|
||||
// successfully synced
|
||||
response.Success = true
|
||||
logging.LogDebug("Finished processing custom browser sync message")
|
||||
return response
|
||||
|
||||
}
|
||||
|
||||
func customBrowserSync(in CustomBrowserSyncMessage) error {
|
||||
//logging.LogDebug("Received connection to c2Sync", "syncMessage", in)
|
||||
customSyncBrowser := databaseStructs.CustomBrowser{}
|
||||
if in.CustomBrowser.Name == "" {
|
||||
logging.LogError(nil, "Can't have custom browser container with empty name - bad sync")
|
||||
return errors.New("Can't have custom browser container with empty name - bad sync")
|
||||
}
|
||||
if !isValidContainerVersion(in.ContainerVersion) {
|
||||
logging.LogError(nil, "attempting to sync bad custom browser container version")
|
||||
return errors.New(fmt.Sprintf("Version, %s, isn't supported. The max supported version is < %s. \nThis likely means your PyPi or Golang library is out of date and should be updated.", in.ContainerVersion, validContainerVersionMax))
|
||||
}
|
||||
err := database.DB.Get(&customSyncBrowser, `SELECT * FROM custombrowser WHERE "name"=$1`, in.CustomBrowser.Name)
|
||||
if errors.Is(sql.ErrNoRows, err) {
|
||||
// this means we don't have the c2 profile, so we need to create it and all the associated components
|
||||
logging.LogDebug("Failed to find custom browser, syncing new data", "c2_profile", in.CustomBrowser)
|
||||
customSyncBrowser.Name = in.CustomBrowser.Name
|
||||
customSyncBrowser.Author = in.CustomBrowser.Author
|
||||
customSyncBrowser.ContainerRunning = true
|
||||
customSyncBrowser.Description = in.CustomBrowser.Description
|
||||
customSyncBrowser.Deleted = false
|
||||
customSyncBrowser.SemVer = in.CustomBrowser.SemVer
|
||||
customSyncBrowser.Type = in.CustomBrowser.Type
|
||||
customSyncBrowser.Separator = in.CustomBrowser.Separator
|
||||
if !slices.Contains(validCustomBrowserTypes, in.CustomBrowser.Type) {
|
||||
logging.LogError(nil, "Bad type in custom browser", "type", in.CustomBrowser.Type)
|
||||
return errors.New("Bad \"type\" in custom browser")
|
||||
}
|
||||
customSyncBrowser.Columns = GetMythicJSONArrayFromStruct(in.CustomBrowser.Columns)
|
||||
customSyncBrowser.DefaultVisibleColumns = GetMythicJSONArrayFromStruct(in.CustomBrowser.DefaultVisibleColumns)
|
||||
customSyncBrowser.ExportFunction = in.CustomBrowser.ExportFunction
|
||||
customSyncBrowser.ExtraTableInputs = GetMythicJSONArrayFromStruct(in.CustomBrowser.ExtraTableInputs)
|
||||
customSyncBrowser.IndicatePartialListing = in.CustomBrowser.IndicatePartialListing
|
||||
customSyncBrowser.ShowCurrentPath = in.CustomBrowser.ShowCurrentPath
|
||||
customSyncBrowser.RowActions = GetMythicJSONArrayFromStruct(in.CustomBrowser.RowActions)
|
||||
statement, err := database.DB.PrepareNamed(`INSERT INTO custombrowser
|
||||
("name",author,container_running,description, deleted, semver, "type", "columns", default_visible_columns,
|
||||
export_function, extra_table_inputs, indicate_partial_listing, show_current_path, row_actions, separator)
|
||||
VALUES (:name, :author, :container_running, :description, :deleted, :semver, :type, :columns, :default_visible_columns,
|
||||
:export_function, :extra_table_inputs, :indicate_partial_listing, :show_current_path, :row_actions, :separator)
|
||||
RETURNING id`,
|
||||
)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to create new custom browser statement")
|
||||
return err
|
||||
}
|
||||
err = statement.Get(&customSyncBrowser.ID, customSyncBrowser)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to create new custom browser")
|
||||
return err
|
||||
}
|
||||
logging.LogDebug("New custom browser", "custom browser", customSyncBrowser)
|
||||
} else if err == nil {
|
||||
// the payload exists in the database, so we need to go down the track of updating/adding/removing information
|
||||
logging.LogDebug("Found custom browser", "custom browser", customSyncBrowser)
|
||||
customSyncBrowser.Author = in.CustomBrowser.Author
|
||||
customSyncBrowser.ContainerRunning = true
|
||||
customSyncBrowser.Description = in.CustomBrowser.Description
|
||||
customSyncBrowser.Deleted = false
|
||||
customSyncBrowser.SemVer = in.CustomBrowser.SemVer
|
||||
customSyncBrowser.Type = in.CustomBrowser.Type
|
||||
customSyncBrowser.Separator = in.CustomBrowser.Separator
|
||||
if !slices.Contains(validCustomBrowserTypes, in.CustomBrowser.Type) {
|
||||
logging.LogError(nil, "Bad type in custom browser", "type", in.CustomBrowser.Type)
|
||||
return errors.New("Bad \"type\" in custom browser")
|
||||
}
|
||||
customSyncBrowser.Columns = GetMythicJSONArrayFromStruct(in.CustomBrowser.Columns)
|
||||
customSyncBrowser.DefaultVisibleColumns = GetMythicJSONArrayFromStruct(in.CustomBrowser.DefaultVisibleColumns)
|
||||
customSyncBrowser.ExportFunction = in.CustomBrowser.ExportFunction
|
||||
customSyncBrowser.ExtraTableInputs = GetMythicJSONArrayFromStruct(in.CustomBrowser.ExtraTableInputs)
|
||||
customSyncBrowser.IndicatePartialListing = in.CustomBrowser.IndicatePartialListing
|
||||
customSyncBrowser.ShowCurrentPath = in.CustomBrowser.ShowCurrentPath
|
||||
customSyncBrowser.RowActions = GetMythicJSONArrayFromStruct(in.CustomBrowser.RowActions)
|
||||
_, err = database.DB.NamedExec(`UPDATE custombrowser SET
|
||||
author=:author, container_running=:container_running, description=:description,deleted=:deleted, semver=:semver,
|
||||
"type"=:type, columns=:columns, default_visible_columns=:default_visible_columns, export_function=:export_function,
|
||||
extra_table_inputs=:extra_table_inputs, indicate_partial_listing=:indicate_partial_listing,
|
||||
show_current_path=:show_current_path, row_actions=:row_actions, separator=:separator
|
||||
WHERE id=:id`, customSyncBrowser,
|
||||
)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to update c2 profile in database")
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
logging.LogError(err, "failed to search for custom browsers")
|
||||
return err
|
||||
}
|
||||
go SendAllOperationsMessage(fmt.Sprintf("Successfully synced %s with container version %s", customSyncBrowser.Name, in.ContainerVersion), 0, "debug", database.MESSAGE_LEVEL_DEBUG, false)
|
||||
go database.ResolveAllOperationsMessage(getDownContainerMessage(customSyncBrowser.Name), 0)
|
||||
|
||||
checkContainerStatusAddCustomBrowserChannel <- customSyncBrowser
|
||||
go CreateGraphQLSpectatorAPITokenAndSendOnStartMessage(customSyncBrowser.Name)
|
||||
go updateCustomBrowserCache()
|
||||
return nil
|
||||
}
|
||||
@@ -2,11 +2,12 @@ package rabbitmq
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
|
||||
"github.com/its-a-feature/Mythic/database"
|
||||
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
|
||||
"github.com/its-a-feature/Mythic/logging"
|
||||
@@ -15,7 +16,7 @@ import (
|
||||
|
||||
type MythicRPCCallbackSearchMessage struct {
|
||||
AgentCallbackUUID string `json:"agent_callback_id"`
|
||||
AgentCallbackID int `json:"callback_id"`
|
||||
CallbackID int `json:"callback_id"`
|
||||
SearchCallbackID *int `json:"search_callback_id"`
|
||||
SearchCallbackDisplayID *int `json:"search_callback_display_id"`
|
||||
SearchCallbackUUID *string `json:"search_callback_uuid"`
|
||||
@@ -90,7 +91,7 @@ func MythicRPCCallbackSearch(input MythicRPCCallbackSearchMessage) MythicRPCCall
|
||||
err := database.DB.Get(&callback, `SELECT
|
||||
operation_id
|
||||
FROM callback
|
||||
WHERE agent_callback_id=$1 OR id=$2`, input.AgentCallbackUUID, input.AgentCallbackID)
|
||||
WHERE agent_callback_id=$1 OR id=$2`, input.AgentCallbackUUID, input.CallbackID)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to find callback UUID")
|
||||
response.Error = err.Error()
|
||||
|
||||
@@ -39,6 +39,7 @@ type MythicRPCCallbackUpdateMessage struct {
|
||||
ImpersonationContext *string `json:"impersonation_context,omitempty"`
|
||||
UpdateLastCheckinTime *bool `json:"update_last_checkin_time,omitempty"`
|
||||
UpdateLastCheckinTimeViaC2Profile *string `json:"update_last_checkin_time_via_c2_profile,omitempty"`
|
||||
Dead *bool `json:"dead,omitempty"`
|
||||
}
|
||||
type MythicRPCCallbackUpdateMessageResponse struct {
|
||||
Success bool `json:"success"`
|
||||
@@ -165,11 +166,14 @@ func MythicRPCCallbackUpdate(input MythicRPCCallbackUpdateMessage) MythicRPCCall
|
||||
if input.ImpersonationContext != nil {
|
||||
callback.ImpersonationContext = *input.ImpersonationContext
|
||||
}
|
||||
if input.Dead != nil {
|
||||
callback.Dead = *input.Dead
|
||||
}
|
||||
if _, err := database.DB.NamedExec(`UPDATE callback SET
|
||||
"user"=:user, host=:host, pid=:pid, ip=:ip, extra_info=:extra_info, sleep_info=:sleep_info, enc_key=:enc_key, dec_key=:dec_key,
|
||||
crypto_type=:crypto_type, external_ip=:external_ip, integrity_level=:integrity_level, os=:os, domain=:domain, architecture=:architecture,
|
||||
description=:description, process_name=:process_name, process_short_name=:process_short_name, cwd=:cwd,
|
||||
impersonation_context=:impersonation_context
|
||||
impersonation_context=:impersonation_context, dead=:dead
|
||||
WHERE id=:id`, callback); err != nil {
|
||||
logging.LogError(err, "Failed to update callback information")
|
||||
response.Error = err.Error()
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package rabbitmq
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/its-a-feature/Mythic/database"
|
||||
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
|
||||
"github.com/its-a-feature/Mythic/logging"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
)
|
||||
|
||||
type MythicRPCCallbackTokenSearchMessage struct {
|
||||
TaskID *int `json:"task_id"` //required
|
||||
CallbackID *int `json:"callback_id"`
|
||||
AgentCallbackID *string `json:"agent_callback_id"`
|
||||
}
|
||||
type MythicRPCCallbackTokenSearchMessageResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error"`
|
||||
CallbackTokens []databaseStructs.Callbacktoken `json:"callbacktokens"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
RabbitMQConnection.AddRPCQueue(RPCQueueStruct{
|
||||
Exchange: MYTHIC_EXCHANGE,
|
||||
Queue: MYTHIC_RPC_CALLBACKTOKEN_SEARCH,
|
||||
RoutingKey: MYTHIC_RPC_CALLBACKTOKEN_SEARCH,
|
||||
Handler: processMythicRPCCallbackTokenSearch,
|
||||
})
|
||||
}
|
||||
|
||||
// Endpoint: MYTHIC_RPC_CALLBACKTOKEN_CREATE
|
||||
func MythicRPCCallbackTokenSearch(input MythicRPCCallbackTokenSearchMessage) MythicRPCCallbackTokenSearchMessageResponse {
|
||||
response := MythicRPCCallbackTokenSearchMessageResponse{
|
||||
Success: false,
|
||||
}
|
||||
callbackID := 0
|
||||
if input.TaskID != nil {
|
||||
task := databaseStructs.Task{}
|
||||
err := database.DB.Get(&task, `SELECT
|
||||
task.id, task.callback_id
|
||||
FROM task
|
||||
WHERE task.id = $1`, input.TaskID)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to fetch task")
|
||||
response.Error = err.Error()
|
||||
return response
|
||||
}
|
||||
callbackID = task.CallbackID
|
||||
} else if input.CallbackID != nil {
|
||||
callbackID = *input.CallbackID
|
||||
} else if input.AgentCallbackID != nil {
|
||||
callback := databaseStructs.Callback{}
|
||||
err := database.DB.Get(&callback, `SELECT
|
||||
callback.id
|
||||
FROM callback
|
||||
WHERE callback.agent_callback_id = $1`, input.AgentCallbackID)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to fetch callback")
|
||||
response.Error = err.Error()
|
||||
return response
|
||||
}
|
||||
callbackID = callback.ID
|
||||
} else {
|
||||
response.Error = "Missing TaskID, CallbackID, or AgentCallbackID"
|
||||
return response
|
||||
}
|
||||
callbackTokens := []databaseStructs.Callbacktoken{}
|
||||
err := database.DB.Select(&callbackTokens,
|
||||
`SELECT
|
||||
callbacktoken.id, callbacktoken.token_id, callbacktoken.callback_id, callbacktoken.task_id,
|
||||
callbacktoken.deleted, callbacktoken.host, callbacktoken.timestamp_created,
|
||||
token.id "token.id", token.task_id "token.task_id", token.deleted "token.deleted",
|
||||
token.host "token.host",
|
||||
token.description "token.description",
|
||||
token.operation_id "token.operation_id",
|
||||
token.timestamp "token.timestamp",
|
||||
token.token_id "token.token_id",
|
||||
token.user "token.user",
|
||||
token.groups "token.groups",
|
||||
token.privileges "token.privileges",
|
||||
token.thread_id "token.thread_id",
|
||||
token.process_id "token.process_id",
|
||||
token.session_id "token.session_id",
|
||||
token.logon_sid "token.logon_sid",
|
||||
token.integrity_level_sid "token.integrity_level_sid",
|
||||
token.app_container_sid "token.app_container_sid",
|
||||
token.app_container_number "token.app_container_number",
|
||||
token.default_dacl "token.default_dacl",
|
||||
token.restricted "token.restricted",
|
||||
token.handle "token.handle",
|
||||
token.capabilities "token.capabilities"
|
||||
FROM callbacktoken
|
||||
JOIN token ON callbacktoken.token_id = token.id
|
||||
WHERE
|
||||
callbacktoken.callback_id = $1 AND callbacktoken.deleted = false`, callbackID)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to fetch callbacktoken")
|
||||
response.Error = err.Error()
|
||||
return response
|
||||
}
|
||||
response.CallbackTokens = callbackTokens
|
||||
response.Success = true
|
||||
return response
|
||||
}
|
||||
func processMythicRPCCallbackTokenSearch(msg amqp.Delivery) interface{} {
|
||||
incomingMessage := MythicRPCCallbackTokenSearchMessage{}
|
||||
responseMsg := MythicRPCCallbackTokenSearchMessageResponse{
|
||||
Success: false,
|
||||
}
|
||||
if err := json.Unmarshal(msg.Body, &incomingMessage); err != nil {
|
||||
logging.LogError(err, "Failed to unmarshal JSON into struct")
|
||||
responseMsg.Error = err.Error()
|
||||
} else {
|
||||
return MythicRPCCallbackTokenSearch(incomingMessage)
|
||||
}
|
||||
return responseMsg
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package rabbitmq
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/its-a-feature/Mythic/database"
|
||||
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
|
||||
"github.com/its-a-feature/Mythic/logging"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
)
|
||||
|
||||
type MythicRPCCustomBrowserSearchMessage struct {
|
||||
TaskID *int `json:"task_id"`
|
||||
OperationID *int `json:"operation_id"`
|
||||
GetAllMatchingChildren bool `json:"all_matching_children"`
|
||||
SearchCustomBrowser MythicRPCCustomBrowserSearchData `json:"custombrowser"`
|
||||
}
|
||||
type MythicRPCCustomBrowserSearchMessageResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error"`
|
||||
CustomBrowserEntries []MythicRPCCustomBrowserSearchDataResponse `json:"custombrowser"`
|
||||
}
|
||||
type MythicRPCCustomBrowserSearchData struct {
|
||||
TreeType string `json:"tree_type" mapstructure:"tree_type"`
|
||||
Host *string `json:"host" mapstructure:"host"`
|
||||
Name *string `json:"name" mapstructure:"name"`
|
||||
ParentPath *string `json:"parent_path" mapstructure:"parent_path"`
|
||||
FullPath *string `json:"full_path" mapstructure:"full_path"`
|
||||
MetadataKey *string `json:"metadata_key" mapstructure:"metadata_key"`
|
||||
MetadataValue interface{} `json:"metadata_value" mapstructure:"metadata_value"`
|
||||
}
|
||||
type MythicRPCCustomBrowserSearchDataResponse struct {
|
||||
TreeType string `json:"tree_type" mapstructure:"tree_type"`
|
||||
Host string `json:"host" mapstructure:"host"`
|
||||
Name string `json:"name" mapstructure:"name"`
|
||||
ParentPath string `json:"parent_path" mapstructure:"parent_path"`
|
||||
FullPath string `json:"full_path" mapstructure:"full_path"`
|
||||
Metadata map[string]interface{} `json:"metadata" mapstructure:"metadata"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
RabbitMQConnection.AddRPCQueue(RPCQueueStruct{
|
||||
Exchange: MYTHIC_EXCHANGE,
|
||||
Queue: CUSTOMBROWSER_SEARCH,
|
||||
RoutingKey: CUSTOMBROWSER_SEARCH,
|
||||
Handler: processMythicRPCCustomBrowserSearch,
|
||||
})
|
||||
}
|
||||
|
||||
// Endpoint: MYTHIC_RPC_PROCESS_SEARCH
|
||||
func MythicRPCCustomBrowserSearch(input MythicRPCCustomBrowserSearchMessage) MythicRPCCustomBrowserSearchMessageResponse {
|
||||
response := MythicRPCCustomBrowserSearchMessageResponse{
|
||||
Success: false,
|
||||
CustomBrowserEntries: []MythicRPCCustomBrowserSearchDataResponse{},
|
||||
}
|
||||
paramDict := make(map[string]interface{})
|
||||
if input.TaskID != nil {
|
||||
task := databaseStructs.Task{}
|
||||
err := database.DB.Get(&task, `SELECT
|
||||
task.id, task.operation_id
|
||||
FROM task
|
||||
WHERE task.id=$1`, *input.TaskID)
|
||||
if err != nil {
|
||||
response.Error = err.Error()
|
||||
return response
|
||||
}
|
||||
paramDict["operation_id"] = task.OperationID
|
||||
} else if input.OperationID != nil {
|
||||
paramDict["operation_id"] = *input.OperationID
|
||||
} else {
|
||||
logging.LogError(nil, "must provide task_id or operation_id")
|
||||
response.Error = "must provide task_id or operation_id"
|
||||
return response
|
||||
}
|
||||
paramDict["tree_type"] = input.SearchCustomBrowser.TreeType
|
||||
searchString := `SELECT * FROM mythictree
|
||||
WHERE operation_id=:operation_id AND tree_type=:tree_type AND deleted=false `
|
||||
if input.SearchCustomBrowser.Host != nil {
|
||||
paramDict["host"] = fmt.Sprintf("%%%s%%", *input.SearchCustomBrowser.Host)
|
||||
searchString += "AND host ILIKE :host "
|
||||
}
|
||||
if input.SearchCustomBrowser.Name != nil {
|
||||
paramDict["name"] = fmt.Sprintf("%%%s%%", *input.SearchCustomBrowser.Name)
|
||||
searchString += "AND \"name\" LIKE :name "
|
||||
}
|
||||
if input.SearchCustomBrowser.ParentPath != nil {
|
||||
if input.GetAllMatchingChildren {
|
||||
paramDict["parent_path"] = fmt.Sprintf("%s%%", *input.SearchCustomBrowser.ParentPath)
|
||||
searchString += "AND (parent_path LIKE :parent_path OR display_path LIKE :parent_path) "
|
||||
} else {
|
||||
paramDict["parent_path"] = fmt.Sprintf("%s", *input.SearchCustomBrowser.ParentPath)
|
||||
searchString += "AND (parent_path=:parent_path OR display_path=:parent_path) "
|
||||
}
|
||||
paramDict["parent_path"] = strings.ReplaceAll(paramDict["parent_path"].(string), "\\", "\\\\")
|
||||
|
||||
}
|
||||
if input.SearchCustomBrowser.FullPath != nil {
|
||||
if input.GetAllMatchingChildren {
|
||||
paramDict["full_path"] = fmt.Sprintf("%%%s%%", *input.SearchCustomBrowser.FullPath)
|
||||
searchString += "AND (full_path LIKE :full_path OR display_path LIKE :full_path) "
|
||||
} else {
|
||||
paramDict["full_path"] = fmt.Sprintf("%s", *input.SearchCustomBrowser.FullPath)
|
||||
searchString += "AND (full_path=:full_path OR display_path=:full_path) "
|
||||
}
|
||||
paramDict["full_path"] = strings.ReplaceAll(paramDict["full_path"].(string), "\\", "\\\\")
|
||||
|
||||
}
|
||||
if input.SearchCustomBrowser.MetadataKey != nil {
|
||||
searchString += "AND metadata->:metadata_key "
|
||||
paramDict["metadata_key"] = *input.SearchCustomBrowser.MetadataKey
|
||||
if input.SearchCustomBrowser.MetadataValue != nil {
|
||||
searchString += " = :metadata_value "
|
||||
paramDict["metadata_value"] = input.SearchCustomBrowser.MetadataValue
|
||||
} else {
|
||||
searchString += " is not null "
|
||||
}
|
||||
}
|
||||
|
||||
searchString += " ORDER BY id DESC"
|
||||
logging.LogInfo("searching", "searchString", searchString, "paramDict", paramDict)
|
||||
rows, err := database.DB.NamedQuery(searchString, paramDict)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to search custombrowser information")
|
||||
response.Error = err.Error()
|
||||
return response
|
||||
}
|
||||
for rows.Next() {
|
||||
searchResult := databaseStructs.MythicTree{}
|
||||
err = rows.StructScan(&searchResult)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to get row from mythic tree for search")
|
||||
continue
|
||||
}
|
||||
logging.LogInfo("found match", "match", searchResult)
|
||||
returnedProcess := MythicRPCCustomBrowserSearchDataResponse{
|
||||
TreeType: searchResult.TreeType,
|
||||
Host: searchResult.Host,
|
||||
Name: string(searchResult.Name),
|
||||
ParentPath: string(searchResult.ParentPath),
|
||||
FullPath: string(searchResult.FullPath),
|
||||
Metadata: searchResult.Metadata.StructValue(),
|
||||
}
|
||||
response.CustomBrowserEntries = append(response.CustomBrowserEntries, returnedProcess)
|
||||
}
|
||||
response.Success = true
|
||||
logging.LogInfo("searched successfully", "response", response)
|
||||
return response
|
||||
}
|
||||
func processMythicRPCCustomBrowserSearch(msg amqp.Delivery) interface{} {
|
||||
incomingMessage := MythicRPCCustomBrowserSearchMessage{}
|
||||
responseMsg := MythicRPCCustomBrowserSearchMessageResponse{
|
||||
Success: false,
|
||||
}
|
||||
err := json.Unmarshal(msg.Body, &incomingMessage)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to unmarshal JSON into struct")
|
||||
responseMsg.Error = err.Error()
|
||||
return responseMsg
|
||||
}
|
||||
return MythicRPCCustomBrowserSearch(incomingMessage)
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package rabbitmq
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/its-a-feature/Mythic/database"
|
||||
@@ -66,18 +68,39 @@ func MythicRPCFileSearch(input MythicRPCFileSearchMessage) MythicRPCFileSearchMe
|
||||
if input.AgentFileID != "" {
|
||||
// the search is for a specific fileID, so just fetch it and return
|
||||
fileMeta := databaseStructs.Filemeta{}
|
||||
if err := database.DB.Get(&fileMeta, `SELECT
|
||||
err := database.DB.Get(&fileMeta, `SELECT
|
||||
filemeta.*
|
||||
FROM filemeta
|
||||
WHERE filemeta.agent_file_id=$1`, input.AgentFileID); err != nil {
|
||||
WHERE filemeta.agent_file_id=$1`, input.AgentFileID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
payload := databaseStructs.Payload{}
|
||||
err = database.DB.Get(&payload, `SELECT
|
||||
filemeta.path "filemeta.path",
|
||||
filemeta.filename "filemeta.filename",
|
||||
filemeta.id "filemeta.id",
|
||||
filemeta.operation_id "filemeta.operation_id"
|
||||
FROM payload
|
||||
JOIN filemeta ON payload.file_id = filemeta.id
|
||||
WHERE payload.uuid=$1`, input.AgentFileID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
logging.LogError(err, "Failed to get file data from the database, it's not a payload uuid or file uuid")
|
||||
response.Error = err.Error()
|
||||
return response
|
||||
} else if err != nil {
|
||||
logging.LogError(err, "Failed to get file data from database")
|
||||
response.Error = err.Error()
|
||||
return response
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to get specified file in MythicRPCFileSearch")
|
||||
response.Error = err.Error()
|
||||
return response
|
||||
} else {
|
||||
response.Success = true
|
||||
response.Files = append(response.Files, convertFileMetaToFileData(fileMeta))
|
||||
return response
|
||||
}
|
||||
response.Success = true
|
||||
response.Files = append(response.Files, convertFileMetaToFileData(fileMeta))
|
||||
return response
|
||||
|
||||
} else if input.CallbackID > 0 {
|
||||
callback := databaseStructs.Callback{ID: input.CallbackID}
|
||||
if err := database.DB.Get(&callback, `SELECT operation_id, id FROM callback WHERE id=$1`, callback.ID); err != nil {
|
||||
|
||||
@@ -35,29 +35,24 @@ func MythicRPCResponseCreate(input MythicRPCResponseCreateMessage) MythicRPCResp
|
||||
response := MythicRPCResponseCreateMessageResponse{
|
||||
Success: false,
|
||||
}
|
||||
databaseResponse := databaseStructs.Response{
|
||||
TaskID: input.TaskID,
|
||||
Response: input.Response,
|
||||
task := databaseStructs.Task{
|
||||
ID: input.TaskID,
|
||||
}
|
||||
if len(input.Response) == 0 {
|
||||
response.Error = "Response must have actual bytes"
|
||||
return response
|
||||
}
|
||||
err := database.DB.Get(&databaseResponse.OperationID, `SELECT operation_id FROM task
|
||||
WHERE id=$1`, input.TaskID)
|
||||
err := database.DB.Get(&task, `SELECT operation_id, agent_task_id FROM task WHERE id=$1`, input.TaskID)
|
||||
if err != nil {
|
||||
logging.LogError(err, "failed to fetch task from database")
|
||||
response.Error = err.Error()
|
||||
return response
|
||||
}
|
||||
_, err = database.DB.NamedExec(`INSERT INTO response
|
||||
(task_id, response, operation_id)
|
||||
VALUES (:task_id, :response, :operation_id)`, databaseResponse)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to create response for task", "response", input.Response)
|
||||
response.Error = err.Error()
|
||||
return response
|
||||
}
|
||||
stringOutput := string(input.Response)
|
||||
handleAgentMessagePostResponseUserOutput(task, agentMessagePostResponse{
|
||||
TaskID: task.AgentTaskID,
|
||||
UserOutput: &stringOutput,
|
||||
}, true)
|
||||
_, _ = database.DB.Exec(`UPDATE task SET timestamp=$2 WHERE id=$1`, input.TaskID, time.Now().UTC())
|
||||
response.Success = true
|
||||
return response
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/its-a-feature/Mythic/database"
|
||||
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
|
||||
@@ -160,6 +161,7 @@ type CommandParameter struct {
|
||||
DynamicQueryFunctionName string `json:"dynamic_query_function"`
|
||||
ParameterGroupInformation []ParameterGroupInfo `json:"parameter_group_info"`
|
||||
LimitCredentialsByType []string `json:"limit_credentials_by_type"`
|
||||
VerifierRegex string `json:"verifier_regex"`
|
||||
}
|
||||
|
||||
type CommandAttribute struct {
|
||||
@@ -224,6 +226,9 @@ func processPayloadSyncMessages(msg amqp.Delivery) interface{} {
|
||||
var messageFormats = []string{"xml", "json"}
|
||||
var agentTypes = []string{"agent", "wrapper", "service", "command_augment"}
|
||||
var messageUUIDLengths = []int{16, 36}
|
||||
var buildParameterMutex sync.Mutex
|
||||
var c2ProfileMutex sync.Mutex
|
||||
var commandsMutex sync.Mutex
|
||||
|
||||
func payloadTypeSync(in PayloadTypeSyncMessage) error {
|
||||
//logging.LogDebug("Received connection to PayloadTypeSync", "syncMessage", in)
|
||||
@@ -234,7 +239,7 @@ func payloadTypeSync(in PayloadTypeSyncMessage) error {
|
||||
}
|
||||
if !isValidContainerVersion(in.ContainerVersion) {
|
||||
logging.LogError(nil, "attempting to sync bad payload container version")
|
||||
return errors.New(fmt.Sprintf("Version, %s, isn't supported. The max supported version is %s. \nThis likely means your PyPi or Golang library is out of date and should be updated.", in.ContainerVersion, validContainerVersionMax))
|
||||
return errors.New(fmt.Sprintf("Version, %s, isn't supported. The max supported version is < %s. \nThis likely means your PyPi or Golang library is out of date and should be updated.", in.ContainerVersion, validContainerVersionMax))
|
||||
}
|
||||
if err := database.DB.Get(&payloadtype, `SELECT * FROM payloadtype WHERE "name"=$1`, in.PayloadType.Name); errors.Is(err, sql.ErrNoRows) {
|
||||
// this means we don't have the payload, so we need to create it and all the associated components
|
||||
@@ -281,13 +286,14 @@ func payloadTypeSync(in PayloadTypeSyncMessage) error {
|
||||
payloadtype.CommandHelpFunction = in.PayloadType.CommandHelpFunction
|
||||
payloadtype.SemVer = in.PayloadType.SemVer
|
||||
payloadtype.SupportedC2 = GetMythicJSONArrayFromStruct(in.PayloadType.SupportedC2Profiles)
|
||||
payloadtype.SupportedWrapping = GetMythicJSONArrayFromStruct(in.PayloadType.SupportedWrapperPayloadTypes)
|
||||
payloadtype.SupportsMultipleC2InBuild = in.PayloadType.SupportsMultipleC2InBuild
|
||||
payloadtype.SupportsMultipleC2InstancesInBuild = in.PayloadType.SupportsMultipleC2InstancesInBuild
|
||||
if statement, err := database.DB.PrepareNamed(`INSERT INTO payloadtype
|
||||
("name",author,container_running,file_extension,mythic_encrypts,note,supported_os,supports_dynamic_loading,wrapper,agent_type,message_format,command_augment_supported_agents,message_uuid_length,use_display_params_for_cli_history,
|
||||
c2_parameter_deviations, command_help_function, supports_multiple_c2_in_build, supports_multiple_c2_instances_in_build, supported_c2, semver)
|
||||
c2_parameter_deviations, command_help_function, supports_multiple_c2_in_build, supports_multiple_c2_instances_in_build, supported_c2, semver, supported_wrapping)
|
||||
VALUES (:name, :author, :container_running, :file_extension, :mythic_encrypts, :note, :supported_os, :supports_dynamic_loading, :wrapper,:agent_type, :message_format, :command_augment_supported_agents, :message_uuid_length, :use_display_params_for_cli_history,
|
||||
:c2_parameter_deviations, :command_help_function, :supports_multiple_c2_in_build, :supports_multiple_c2_instances_in_build, :supported_c2, :semver)
|
||||
:c2_parameter_deviations, :command_help_function, :supports_multiple_c2_in_build, :supports_multiple_c2_instances_in_build, :supported_c2, :semver, :supported_wrapping)
|
||||
RETURNING id`,
|
||||
); err != nil {
|
||||
logging.LogError(err, "Failed to create new payloadtype statement")
|
||||
@@ -348,6 +354,7 @@ func payloadTypeSync(in PayloadTypeSyncMessage) error {
|
||||
payloadtype.CommandHelpFunction = in.PayloadType.CommandHelpFunction
|
||||
payloadtype.SemVer = in.PayloadType.SemVer
|
||||
payloadtype.SupportedC2 = GetMythicJSONArrayFromStruct(in.PayloadType.SupportedC2Profiles)
|
||||
payloadtype.SupportedWrapping = GetMythicJSONArrayFromStruct(in.PayloadType.SupportedWrapperPayloadTypes)
|
||||
payloadtype.SupportsMultipleC2InBuild = in.PayloadType.SupportsMultipleC2InBuild
|
||||
payloadtype.SupportsMultipleC2InstancesInBuild = in.PayloadType.SupportsMultipleC2InstancesInBuild
|
||||
_, err = database.DB.NamedExec(`UPDATE payloadtype SET
|
||||
@@ -357,7 +364,7 @@ func payloadTypeSync(in PayloadTypeSyncMessage) error {
|
||||
message_uuid_length=:message_uuid_length, use_display_params_for_cli_history=:use_display_params_for_cli_history,
|
||||
c2_parameter_deviations=:c2_parameter_deviations, command_help_function=:command_help_function,
|
||||
supports_multiple_c2_in_build=:supports_multiple_c2_in_build, supports_multiple_c2_instances_in_build=:supports_multiple_c2_instances_in_build,
|
||||
supported_c2=:supported_c2, semver=:semver
|
||||
supported_c2=:supported_c2, semver=:semver, supported_wrapping=:supported_wrapping
|
||||
WHERE id=:id`, payloadtype,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -459,6 +466,8 @@ func updatePayloadTypeBuildParameters(in PayloadTypeSyncMessage, payloadtype dat
|
||||
// if a parameter is in the database but not in the sync message, delete it
|
||||
// if a parameter is in the sync message, but not in the database, add it
|
||||
// if a parameter is in both, update it
|
||||
buildParameterMutex.Lock()
|
||||
defer buildParameterMutex.Unlock()
|
||||
syncingParameters := in.PayloadType.BuildParameters
|
||||
databaseParameter := databaseStructs.Buildparameter{}
|
||||
updatedAndDeletedParameters := []string{}
|
||||
@@ -478,7 +487,7 @@ func updatePayloadTypeBuildParameters(in PayloadTypeSyncMessage, payloadtype dat
|
||||
} else {
|
||||
//logging.LogDebug("Got row from buildparameter while syncing payloadtype", "row", databaseParameter)
|
||||
for _, newParameter := range syncingParameters {
|
||||
if newParameter.Name == databaseParameter.Name {
|
||||
if newParameter.Name == databaseParameter.Name && !utils.SliceContains(updatedAndDeletedParameters, newParameter.Name) {
|
||||
// we found a matching parameter name, update it
|
||||
//logging.LogDebug("Found matching newParameter.Name and databaseParameter.Name", "name", newParameter.Name)
|
||||
updatedAndDeletedParameters = append(updatedAndDeletedParameters, databaseParameter.Name)
|
||||
@@ -521,6 +530,7 @@ func updatePayloadTypeBuildParameters(in PayloadTypeSyncMessage, payloadtype dat
|
||||
logging.LogError(err, "Failed to update build parameter in database", "build_parameter", databaseParameter)
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -599,6 +609,8 @@ func updatePayloadTypeC2Profiles(in PayloadTypeSyncMessage, payloadtype database
|
||||
// get all currently associated c2 profiles from database
|
||||
// if profile in database but not in sync message, delete it
|
||||
// if profile in sync message but not in database, add it
|
||||
c2ProfileMutex.Lock()
|
||||
defer c2ProfileMutex.Unlock()
|
||||
syncingC2Profiles := in.PayloadType.SupportedC2Profiles
|
||||
databaseC2Profile := databaseStructs.Payloadtypec2profile{}
|
||||
if rows, err := database.DB.NamedQuery(`SELECT
|
||||
@@ -761,6 +773,8 @@ func updatePayloadTypeCommands(in PayloadTypeSyncMessage, payloadtype databaseSt
|
||||
// if a command is in the database but not in the sync message, delete it
|
||||
// if command is in the sync message, but not in the database, then add it
|
||||
// if command is in both, update it
|
||||
commandsMutex.Lock()
|
||||
defer commandsMutex.Unlock()
|
||||
syncingCommands := in.CommandList
|
||||
databaseCommand := databaseStructs.Command{}
|
||||
updatedAndDeletedCommands := []string{}
|
||||
@@ -987,12 +1001,14 @@ func updatePayloadTypeCommandParameters(in PayloadTypeSyncMessage, payloadtype d
|
||||
databaseParameter.ChoiceFilterByCommandAttributes = GetMythicJSONTextFromStruct(newParameter.FilterCommandChoicesByCommandAttributes)
|
||||
databaseParameter.DynamicQueryFunction = newParameter.DynamicQueryFunctionName
|
||||
databaseParameter.SupportedAgentBuildParameters = GetMythicJSONTextFromStruct(newParameter.SupportedAgentBuildParameters)
|
||||
databaseParameter.VerifierRegex = newParameter.VerifierRegex
|
||||
_, err = database.DB.NamedExec(`UPDATE commandparameters SET
|
||||
cli_name=:cli_name, display_name=:display_name, description=:description, choices=:choices, default_value=:default_value,
|
||||
supported_agents=:supported_agents, supported_agent_build_parameters=:supported_agent_build_parameters,
|
||||
choices_are_all_commands=:choices_are_all_commands, choices_are_loaded_commands=:choices_are_loaded_commands,
|
||||
choice_filter_by_command_attributes=:choice_filter_by_command_attributes, dynamic_query_function=:dynamic_query_function,
|
||||
required=:required, ui_position=:ui_position, "type"=:type, limit_credentials_by_type=:limit_credentials_by_type
|
||||
required=:required, ui_position=:ui_position, "type"=:type, limit_credentials_by_type=:limit_credentials_by_type,
|
||||
verifier_regex=:verifier_regex
|
||||
WHERE id=:id`, databaseParameter,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -1045,6 +1061,7 @@ func updatePayloadTypeCommandParameters(in PayloadTypeSyncMessage, payloadtype d
|
||||
ParameterGroupName: newParameterGroup.GroupName,
|
||||
CommandID: command.ID,
|
||||
DynamicQueryFunction: newParameter.DynamicQueryFunctionName,
|
||||
VerifierRegex: newParameter.VerifierRegex,
|
||||
}
|
||||
if defaultVal, err := getSyncToDatabaseValueForDefaultValue(newParameter.ParameterType, newParameter.DefaultValue, newParameter.Choices); err != nil {
|
||||
logging.LogError(err, "Failed to getSyncToDatabaseValueForDefaultValue for brand new command parameter", "newParameter", newParameter)
|
||||
@@ -1057,10 +1074,10 @@ func updatePayloadTypeCommandParameters(in PayloadTypeSyncMessage, payloadtype d
|
||||
if statement, err := database.DB.PrepareNamed(`INSERT INTO commandparameters
|
||||
("name",display_name,cli_name,description,command_id,choices,default_value,supported_agents,choices_are_all_commands,
|
||||
choices_are_loaded_commands,required,ui_position,"type",choice_filter_by_command_attributes,dynamic_query_function,supported_agent_build_parameters,parameter_group_name,
|
||||
limit_credentials_by_type)
|
||||
limit_credentials_by_type, verifier_regex)
|
||||
VALUES (:name, :display_name, :cli_name, :description, :command_id, :choices, :default_value, :supported_agents, :choices_are_all_commands,
|
||||
:choices_are_loaded_commands, :required, :ui_position, :type, :choice_filter_by_command_attributes, :dynamic_query_function, :supported_agent_build_parameters, :parameter_group_name,
|
||||
:limit_credentials_by_type)
|
||||
:limit_credentials_by_type, :verifier_regex)
|
||||
RETURNING id`,
|
||||
); err != nil {
|
||||
logging.LogError(err, "Failed to create new command parameters statement when importing payloadtype")
|
||||
@@ -1358,19 +1375,29 @@ func updateAllCallbacksWithCommandAugments() {
|
||||
func reSyncPayloadTypes() {
|
||||
payloadTypes := []databaseStructs.Payloadtype{}
|
||||
if err := database.DB.Select(&payloadTypes, `SELECT
|
||||
"name", container_running, wrapper
|
||||
FROM payloadtype
|
||||
WHERE deleted=false`); err != nil {
|
||||
"name", supported_wrapping, supported_c2, id
|
||||
FROM payloadtype`); err != nil {
|
||||
logging.LogError(err, "Failed to fetch payload types from database")
|
||||
} else {
|
||||
for _, pt := range payloadTypes {
|
||||
if pt.ContainerRunning {
|
||||
if pt.Wrapper {
|
||||
continue
|
||||
}
|
||||
if _, err = RabbitMQConnection.SendPTRPCReSync(PTRPCReSyncMessage{Name: pt.Name}); err != nil {
|
||||
logging.LogError(err, "Failed to ask payload type to resync")
|
||||
}
|
||||
err = updatePayloadTypeC2Profiles(PayloadTypeSyncMessage{
|
||||
PayloadType: PayloadType{
|
||||
Name: pt.Name,
|
||||
SupportedC2Profiles: pt.SupportedC2.StructStringValue(),
|
||||
},
|
||||
},
|
||||
pt,
|
||||
)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to update payload type c2 mapping")
|
||||
}
|
||||
err = updatePayloadTypeWrappers(PayloadTypeSyncMessage{
|
||||
PayloadType: PayloadType{
|
||||
Name: pt.Name,
|
||||
SupportedWrapperPayloadTypes: pt.SupportedWrapping.StructStringValue(),
|
||||
}}, pt)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to update payload type wrapper mapping")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/its-a-feature/Mythic/database"
|
||||
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
|
||||
|
||||
@@ -79,7 +80,7 @@ func trSync(in TrSyncMessage) error {
|
||||
}
|
||||
if !isValidContainerVersion(in.ContainerVersion) {
|
||||
logging.LogError(nil, "attempting to sync bad translation container version")
|
||||
return errors.New(fmt.Sprintf("Version, %s, isn't supported. The max supported version is %s. \nThis likely means your PyPi or Golang library is out of date and should be updated.", in.ContainerVersion, validContainerVersionMax))
|
||||
return errors.New(fmt.Sprintf("Version, %s, isn't supported. The max supported version is < %s. \nThis likely means your PyPi or Golang library is out of date and should be updated.", in.ContainerVersion, validContainerVersionMax))
|
||||
}
|
||||
err := database.DB.Get(&translationDatabase, `SELECT id
|
||||
FROM translationcontainer
|
||||
|
||||
@@ -25,21 +25,25 @@ type C2HostFileMessageResponse struct {
|
||||
func (r *rabbitMQConnection) SendC2RPCHostFile(hostFile C2HostFileMessage) (*C2HostFileMessageResponse, error) {
|
||||
c2HostFileMessageResponse := C2HostFileMessageResponse{}
|
||||
exclusiveQueue := true
|
||||
if opsecBytes, err := json.Marshal(hostFile); err != nil {
|
||||
opsecBytes, err := json.Marshal(hostFile)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to convert hostFile to JSON", "hostFile", hostFile)
|
||||
return nil, err
|
||||
} else if response, err := r.SendRPCMessage(
|
||||
}
|
||||
response, err := r.SendRPCMessage(
|
||||
MYTHIC_EXCHANGE,
|
||||
GetC2RPCHostFileRoutingKey(hostFile.Name),
|
||||
opsecBytes,
|
||||
exclusiveQueue,
|
||||
); err != nil {
|
||||
)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to send RPC message")
|
||||
return nil, err
|
||||
} else if err := json.Unmarshal(response, &c2HostFileMessageResponse); err != nil {
|
||||
}
|
||||
err = json.Unmarshal(response, &c2HostFileMessageResponse)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to parse c2 host file response back to struct", "response", response)
|
||||
return nil, err
|
||||
} else {
|
||||
return &c2HostFileMessageResponse, nil
|
||||
}
|
||||
return &c2HostFileMessageResponse, nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package rabbitmq
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/its-a-feature/Mythic/database"
|
||||
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
|
||||
|
||||
@@ -101,5 +102,6 @@ func (r *rabbitMQConnection) SendC2RPCStartServer(startServer C2StartServerMessa
|
||||
logging.LogError(err, "Failed to parse start server response back to struct", "response", response)
|
||||
return &c2StartServerResponse, err
|
||||
}
|
||||
logging.LogDebug("Got response from start server request", "startServer", startServer)
|
||||
return &c2StartServerResponse, nil
|
||||
}
|
||||
|
||||
@@ -24,21 +24,25 @@ type C2StopServerMessageResponse struct {
|
||||
func (r *rabbitMQConnection) SendC2RPCStopServer(stopServer C2StopServerMessage) (*C2StopServerMessageResponse, error) {
|
||||
c2StopServerResponse := C2StopServerMessageResponse{}
|
||||
exclusiveQueue := true
|
||||
if opsecBytes, err := json.Marshal(stopServer); err != nil {
|
||||
opsecBytes, err := json.Marshal(stopServer)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to convert stopServer to JSON", "stopServer", stopServer)
|
||||
return &c2StopServerResponse, err
|
||||
} else if response, err := r.SendRPCMessage(
|
||||
}
|
||||
response, err := r.SendRPCMessage(
|
||||
MYTHIC_EXCHANGE,
|
||||
GetC2RPCStopServerRoutingKey(stopServer.Name),
|
||||
opsecBytes,
|
||||
exclusiveQueue,
|
||||
); err != nil {
|
||||
)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to send RPC message")
|
||||
return &c2StopServerResponse, err
|
||||
} else if err := json.Unmarshal(response, &c2StopServerResponse); err != nil {
|
||||
}
|
||||
err = json.Unmarshal(response, &c2StopServerResponse)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to parse c2 stop server response back to struct", "response", response)
|
||||
return &c2StopServerResponse, err
|
||||
} else {
|
||||
return &c2StopServerResponse, nil
|
||||
}
|
||||
return &c2StopServerResponse, nil
|
||||
}
|
||||
|
||||
@@ -13,16 +13,15 @@ type ContainerOnStartMessage struct {
|
||||
}
|
||||
|
||||
func (r *rabbitMQConnection) SendContainerOnStart(onStartMessage ContainerOnStartMessage) error {
|
||||
if err := r.SendStructMessage(
|
||||
err := r.SendStructMessage(
|
||||
MYTHIC_EXCHANGE,
|
||||
GetContainerOnStartRoutingKey(onStartMessage.ContainerName),
|
||||
"",
|
||||
onStartMessage,
|
||||
true,
|
||||
); err != nil {
|
||||
)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to send message")
|
||||
return err
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package rabbitmq
|
||||
|
||||
type ExportFunctionMessage struct {
|
||||
TreeType string `json:"tree_type"`
|
||||
ContainerName string `json:"container_name"`
|
||||
Host string `json:"host"`
|
||||
Path string `json:"path"`
|
||||
OperationID int `json:"operation_id"`
|
||||
}
|
||||
type ExportFunctionMessageResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error"`
|
||||
OperationID int `json:"operation_id"`
|
||||
}
|
||||
|
||||
func (r *rabbitMQConnection) SendCbExportFunction(msg ExportFunctionMessage) error {
|
||||
err := r.SendStructMessage(
|
||||
MYTHIC_EXCHANGE,
|
||||
GetCustomBrowserExportFunctionRoutingKey(msg.ContainerName),
|
||||
"",
|
||||
msg,
|
||||
false,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -3,9 +3,10 @@ package rabbitmq
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/its-a-feature/Mythic/eventing"
|
||||
"time"
|
||||
|
||||
"github.com/its-a-feature/Mythic/eventing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/its-a-feature/Mythic/database"
|
||||
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
|
||||
@@ -18,7 +19,7 @@ func RegisterNewPayload(payloadDefinition PayloadConfiguration, operatorOperatio
|
||||
payloadtype := databaseStructs.Payloadtype{}
|
||||
var err error
|
||||
err = database.DB.Get(&payloadtype, `SELECT
|
||||
payloadtype.id, payloadtype."name", "wrapper", supported_os, supports_dynamic_loading, translation_container_id, mythic_encrypts
|
||||
payloadtype.id, payloadtype."name", "wrapper", supported_os, supports_dynamic_loading, translation_container_id, mythic_encrypts, semver
|
||||
FROM payloadtype
|
||||
WHERE payloadtype."name"=$1`, payloadDefinition.PayloadType)
|
||||
if err != nil {
|
||||
@@ -107,14 +108,15 @@ func RegisterNewPayload(payloadDefinition PayloadConfiguration, operatorOperatio
|
||||
return "", 0, err
|
||||
}
|
||||
databasePayload := databaseStructs.Payload{
|
||||
OperatorID: operatorOperation.CurrentOperator.ID,
|
||||
PayloadTypeID: payloadtype.ID,
|
||||
Description: payloadDefinition.Description,
|
||||
UuID: payloadDefinition.UUID,
|
||||
OperationID: operatorOperation.CurrentOperation.ID,
|
||||
Os: payloadDefinition.SelectedOS,
|
||||
Payloadtype: payloadtype,
|
||||
BuildContainer: "rabbitmq",
|
||||
OperatorID: operatorOperation.CurrentOperator.ID,
|
||||
PayloadTypeID: payloadtype.ID,
|
||||
Description: payloadDefinition.Description,
|
||||
UuID: payloadDefinition.UUID,
|
||||
OperationID: operatorOperation.CurrentOperation.ID,
|
||||
Os: payloadDefinition.SelectedOS,
|
||||
Payloadtype: payloadtype,
|
||||
BuildContainer: "rabbitmq",
|
||||
PayloadTypeSemver: payloadtype.SemVer,
|
||||
}
|
||||
databasePayload.FileID.Valid = true
|
||||
databasePayload.FileID.Int64 = int64(fileMeta.ID)
|
||||
@@ -128,8 +130,8 @@ func RegisterNewPayload(payloadDefinition PayloadConfiguration, operatorOperatio
|
||||
databasePayload.EventStepInstanceID.Int64 = int64(payloadDefinition.EventStepInstance)
|
||||
}
|
||||
statement, err = database.DB.PrepareNamed(`INSERT INTO payload
|
||||
(operator_id, payload_type_id, description, uuid, operation_id, os, file_id, wrapped_payload_id, build_container, eventstepinstance_id)
|
||||
VALUES (:operator_id, :payload_type_id, :description, :uuid, :operation_id, :os, :file_id, :wrapped_payload_id, :build_container, :eventstepinstance_id)
|
||||
(operator_id, payload_type_id, description, uuid, operation_id, os, file_id, wrapped_payload_id, build_container, eventstepinstance_id, payload_type_semver)
|
||||
VALUES (:operator_id, :payload_type_id, :description, :uuid, :operation_id, :os, :file_id, :wrapped_payload_id, :build_container, :eventstepinstance_id, :payload_type_semver)
|
||||
RETURNING id`,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -393,13 +393,12 @@ func processAgentMessageContent(agentMessageInput *AgentMessageRawInput, uuidInf
|
||||
finalBytes, err := handleAgentMessageStagingTranslation(&decryptedMessage, uuidInfo)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to handle translation staging function")
|
||||
go SendAllOperationsMessage(err.Error(), uuidInfo.OperationID, "debug", database.MESSAGE_LEVEL_AGENT_MESSGAGE, true)
|
||||
go SendAllOperationsMessage(err.Error(), uuidInfo.OperationID, "agent_message_bad_staging", database.MESSAGE_LEVEL_AGENT_MESSGAGE, true)
|
||||
instanceResponse.Err = err
|
||||
return response
|
||||
}
|
||||
instanceResponse.Message = *finalBytes
|
||||
return response
|
||||
|
||||
}
|
||||
default:
|
||||
{
|
||||
@@ -587,9 +586,11 @@ func recursiveProcessAgentMessage(agentMessageInput *AgentMessageRawInput) recur
|
||||
if agentMessageInput.Base64Message != nil {
|
||||
base64DecodedMessage, err = base64.StdEncoding.DecodeString(string(*agentMessageInput.Base64Message))
|
||||
if err != nil {
|
||||
errMsg := err.Error()
|
||||
base64DecodedMessage, err = base64.URLEncoding.DecodeString(string(*agentMessageInput.Base64Message))
|
||||
if err != nil {
|
||||
errorMessage := fmt.Sprintf("Failed to base64 decode message\n")
|
||||
errorMessage := fmt.Sprintf("Failed to base64 decode message\nTried to process base64 message as standard encoding and url safe encoding:")
|
||||
errorMessage += fmt.Sprintf("StdBase64 Error: %s\nURLEncoded Base64 Error: %s\n", errMsg, err.Error())
|
||||
errorMessage += fmt.Sprintf("message: %s\n", string(*agentMessageInput.Base64Message))
|
||||
errorMessage += fmt.Sprintf("Connection from %s via %s\n", agentMessageInput.RemoteIP, agentMessageInput.C2Profile)
|
||||
logging.LogError(err, "Failed to base64 decode agent message")
|
||||
|
||||
@@ -64,6 +64,7 @@ type agentMessagePostResponse struct {
|
||||
Alerts *[]agentMessagePostResponseAlert `json:"alerts,omitempty" mapstructure:"alerts,omitempty" xml:"alerts,omitempty"`
|
||||
Callback *agentMessagePostResponseCallback `json:"callback,omitempty" mapstructure:"callback,omitempty" xml:"callback,omitempty"`
|
||||
Events *[]agentMessagePostResponseTriggerEvent `json:"events,omitempty" mapstructure:"events,omitempty" xml:"events,omitempty"`
|
||||
CustomBrowser *agentMessagePostResponseCustomBrowser `json:"custom_browser,omitempty" mapstructure:"custom_browser,omitempty" xml:"custom_browser,omitempty"`
|
||||
Other map[string]interface{} `json:"-" mapstructure:",remain"` // capture any 'other' keys that were passed in so we can reply back with them
|
||||
}
|
||||
|
||||
@@ -151,7 +152,7 @@ type agentMessagePostResponseKeylogs struct {
|
||||
}
|
||||
type agentMessagePostResponseToken struct {
|
||||
Action string `json:"action" mapstructure:"action" xml:"action"`
|
||||
TokenID uint64 `json:"token_id" mapstructure:"token_id" xml:"token_id"`
|
||||
TokenID int64 `json:"token_id" mapstructure:"token_id" xml:"token_id"`
|
||||
User string `json:"user" mapstructure:"user" xml:"user"`
|
||||
Groups string `json:"groups" mapstructure:"groups" xml:"groups"`
|
||||
Privileges string `json:"privileges" mapstructure:"privileges" xml:"privileges"`
|
||||
@@ -170,7 +171,7 @@ type agentMessagePostResponseToken struct {
|
||||
type agentMessagePostResponseCallbackTokens struct {
|
||||
Action string `json:"action" mapstructure:"action" xml:"action"`
|
||||
Host *string `json:"host,omitempty" mapstructure:"host,omitempty" xml:"host,omitempty"`
|
||||
TokenId uint64 `json:"token_id" mapstructure:"token_id" xml:"token_id"`
|
||||
TokenId int64 `json:"token_id" mapstructure:"token_id" xml:"token_id"`
|
||||
// optionally also provide all the token information
|
||||
TokenInfo *agentMessagePostResponseToken `mapstructure:"token"`
|
||||
}
|
||||
@@ -233,6 +234,28 @@ type agentMessagePostResponseTriggerEvent struct {
|
||||
Keyword string `json:"keyword" mapstructure:"keyword" xml:"keyword"`
|
||||
Data map[string]interface{} `json:"data" mapstructure:"data" xml:"data"`
|
||||
}
|
||||
type agentMessagePostResponseCustomBrowser struct {
|
||||
BrowserName string `json:"browser_name" mapstructure:"browser_name" xml:"browser_name"`
|
||||
Host string `json:"host" mapstructure:"host" xml:"host"`
|
||||
UpdateDeleted bool `json:"update_deleted" mapstructure:"update_deleted" xml:"update_deleted"` // option to treat this response as full source of truth
|
||||
SetAsUserOutput bool `json:"set_as_user_output" mapstructure:"set_as_user_output"`
|
||||
Entries *[]agentMessagePostResponseCustomBrowserEntry `json:"entries" mapstructure:"entries" xml:"entries"`
|
||||
}
|
||||
type agentMessagePostResponseCustomBrowserEntry struct {
|
||||
Name string `json:"name" mapstructure:"name" xml:"name"`
|
||||
ParentPath string `json:"parent_path" mapstructure:"parent_path" xml:"parent_path"`
|
||||
DisplayPath string `json:"display_path" mapstructure:"display_path" xml:"display_path"`
|
||||
Success *bool `json:"success,omitempty" mapstructure:"success,omitempty" xml:"success,omitempty"`
|
||||
CanHaveChildren bool `json:"can_have_children,omitempty" mapstructure:"can_have_children,omitempty" xml:"can_have_children,omitempty"`
|
||||
Metadata interface{} `json:"metadata" mapstructure:"metadata" xml:"metadata"`
|
||||
Children *[]agentMessagePostResponseCustomBrowserChildren `json:"children,omitempty" mapstructure:"children,omitempty" xml:"children,omitempty"`
|
||||
}
|
||||
type agentMessagePostResponseCustomBrowserChildren struct {
|
||||
Name string `json:"name" mapstructure:"name" xml:"name"`
|
||||
DisplayPath string `json:"display_path" mapstructure:"display_path" xml:"display_path"`
|
||||
CanHaveChildren bool `json:"can_have_children,omitempty" mapstructure:"can_have_children,omitempty" xml:"can_have_children,omitempty"`
|
||||
Metadata interface{} `json:"metadata" mapstructure:"metadata" xml:"metadata"`
|
||||
}
|
||||
|
||||
// writeDownloadChunkToDiskChan is a blocking call intentionally
|
||||
type writeDownloadChunkToDisk struct {
|
||||
@@ -511,6 +534,9 @@ func handleAgentMessagePostResponse(incoming *map[string]interface{}, uUIDInfo *
|
||||
if agentMessage.Responses[i].Events != nil && len(*agentMessage.Responses[i].Events) > 0 {
|
||||
go handleAgentMessagePostResponseEvent(currentTask, agentMessage.Responses[i].Events)
|
||||
}
|
||||
if agentMessage.Responses[i].CustomBrowser != nil {
|
||||
go handleAgentMessagePostResponseCustomBrowser(currentTask, agentMessage.Responses[i].CustomBrowser)
|
||||
}
|
||||
// this section always happens
|
||||
reflectBackOtherKeys(&mythicResponse, &agentMessage.Responses[i].Other)
|
||||
responses = append(responses, mythicResponse)
|
||||
@@ -556,7 +582,7 @@ func handleAgentMessagePostResponse(incoming *map[string]interface{}, uUIDInfo *
|
||||
fileMeta.Size = POSTGRES_MAX_BIGINT - 1
|
||||
}
|
||||
}
|
||||
if fileMeta.ChunksReceived >= fileMeta.TotalChunks {
|
||||
if fileMeta.ChunksReceived >= fileMeta.TotalChunks && fileMeta.TotalChunks >= 0 {
|
||||
fileMeta.Complete = true
|
||||
// also calculate new md5 and sha1 sums
|
||||
sha1Hash := sha1.New()
|
||||
@@ -1291,6 +1317,9 @@ func handleAgentMessagePostResponseDownload(task *databaseStructs.Task, agentRes
|
||||
if agentResponse.Download.IsScreenshot != nil && *agentResponse.Download.IsScreenshot {
|
||||
fileMeta.IsScreenshot = *agentResponse.Download.IsScreenshot
|
||||
}
|
||||
if fileMeta.TotalChunks < 0 && agentResponse.Download.TotalChunks != nil && *agentResponse.Download.TotalChunks > 0 {
|
||||
fileMeta.TotalChunks = *agentResponse.Download.TotalChunks
|
||||
}
|
||||
return handleAgentMessageWriteDownloadChunkToLocalDisk(task, fileMeta, agentResponse)
|
||||
|
||||
} else if agentResponse.Download.TotalChunks != nil {
|
||||
@@ -1606,7 +1635,7 @@ func updateFileMetaFromUpload(fileMeta databaseStructs.Filemeta, task databaseSt
|
||||
}
|
||||
}
|
||||
func associateFileMetaWithMythicTree(pathData utils.AnalyzedPath, fileMeta databaseStructs.Filemeta, task databaseStructs.Task) {
|
||||
resolveAndCreateParentPathsForTreeNode(pathData, task, databaseStructs.TREE_TYPE_FILE)
|
||||
resolveAndCreateParentPathsForTreeNode(pathData, utils.AnalyzedPath{}, task, databaseStructs.TREE_TYPE_FILE)
|
||||
newTree := databaseStructs.MythicTree{
|
||||
Host: pathData.Host,
|
||||
TaskID: task.ID,
|
||||
@@ -1728,7 +1757,7 @@ func HandleAgentMessagePostResponseFileBrowser(task databaseStructs.Task, fileBr
|
||||
if fileBrowser.Host != "" {
|
||||
pathData.Host = strings.ToUpper(fileBrowser.Host)
|
||||
}
|
||||
resolveAndCreateParentPathsForTreeNode(pathData, task, databaseStructs.TREE_TYPE_FILE)
|
||||
resolveAndCreateParentPathsForTreeNode(pathData, utils.AnalyzedPath{}, task, databaseStructs.TREE_TYPE_FILE)
|
||||
// now that the parents and all ancestors are resolved, process the current path and all children
|
||||
realParentPath := strings.Join(pathData.PathPieces, pathData.PathSeparator)
|
||||
// check for the instance of // as a leading path
|
||||
@@ -1759,6 +1788,7 @@ func HandleAgentMessagePostResponseFileBrowser(task databaseStructs.Task, fileBr
|
||||
TreeType: databaseStructs.TREE_TYPE_FILE,
|
||||
CanHaveChildren: !fileBrowser.IsFile,
|
||||
Deleted: false,
|
||||
HasChildren: !fileBrowser.IsFile && fileBrowser.Files != nil && len(*fileBrowser.Files) > 0,
|
||||
Os: getOSTypeBasedOnPathSeparator(pathData.PathSeparator, databaseStructs.TREE_TYPE_FILE),
|
||||
}
|
||||
if fileBrowser.Success != nil {
|
||||
@@ -2139,12 +2169,13 @@ func HandleAgentMessagePostResponseProcesses(task databaseStructs.Task, processe
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func resolveAndCreateParentPathsForTreeNode(pathData utils.AnalyzedPath, task databaseStructs.Task, treeType string) {
|
||||
func resolveAndCreateParentPathsForTreeNode(pathData utils.AnalyzedPath, displayPathData utils.AnalyzedPath, task databaseStructs.Task, treeType string) {
|
||||
for i, _ := range pathData.PathPieces {
|
||||
parentPath, fullPath, name := getParentPathFullPathName(pathData, i, databaseStructs.TREE_TYPE_FILE)
|
||||
parentPath, fullPath, name := getParentPathFullPathName(pathData, i, treeType)
|
||||
if parentPath == "" && fullPath == "" && name == "" {
|
||||
continue
|
||||
}
|
||||
displayParentPath, displayFullPath, displayName := getParentPathFullPathName(displayPathData, i, treeType)
|
||||
newTree := databaseStructs.MythicTree{
|
||||
Host: pathData.Host,
|
||||
TaskID: task.ID,
|
||||
@@ -2155,26 +2186,33 @@ func resolveAndCreateParentPathsForTreeNode(pathData utils.AnalyzedPath, task da
|
||||
TreeType: treeType,
|
||||
CanHaveChildren: true,
|
||||
Deleted: false,
|
||||
HasChildren: true,
|
||||
DisplayPath: []byte(displayFullPath),
|
||||
}
|
||||
metadata := map[string]interface{}{
|
||||
"has_children": true,
|
||||
}
|
||||
newTree.Metadata = GetMythicJSONTextFromStruct(metadata)
|
||||
newTree.Metadata = GetMythicJSONTextFromStruct(nil)
|
||||
newTree.Os = getOSTypeBasedOnPathSeparator(pathData.PathSeparator, treeType)
|
||||
newTree.CallbackID.Valid = true
|
||||
newTree.CallbackID.Int64 = int64(task.Callback.ID)
|
||||
logging.LogInfo("creating parent", "name", name, "parentPath", parentPath, "fullPath", fullPath)
|
||||
logging.LogInfo("display path data for parent path", "displayParentPath", displayParentPath, "displayFullPath", displayFullPath, "displayName", displayName)
|
||||
createTreeNode(&newTree)
|
||||
}
|
||||
}
|
||||
func getOSTypeBasedOnPathSeparator(pathSeparator string, treeType string) string {
|
||||
switch treeType {
|
||||
case databaseStructs.TREE_TYPE_FILE:
|
||||
fallthrough
|
||||
case databaseStructs.TREE_TYPE_PROCESS:
|
||||
if pathSeparator == "/" {
|
||||
return "linux"
|
||||
} else {
|
||||
return "windows"
|
||||
}
|
||||
default:
|
||||
custom, ok := getCustomBrowser(treeType)
|
||||
if ok {
|
||||
return custom.Os
|
||||
}
|
||||
return "linux"
|
||||
}
|
||||
}
|
||||
@@ -2197,11 +2235,29 @@ func treeNodeGetFullPath(parentPath []byte, name []byte, pathSeparator []byte, t
|
||||
}
|
||||
}
|
||||
return fullPath
|
||||
|
||||
case databaseStructs.TREE_TYPE_PROCESS:
|
||||
// full path is just the process_id of the current process
|
||||
return name
|
||||
default:
|
||||
custom, ok := getCustomBrowser(treeType)
|
||||
if ok && custom.Type == databaseStructs.TREE_TYPE_FILE {
|
||||
fullPath := parentPath
|
||||
// if parent path is empty, then we don't want to add an extra instance of the path separator
|
||||
if len(fullPath) > 0 {
|
||||
if fullPath[len(fullPath)-1] == pathSeparator[len(pathSeparator)-1] {
|
||||
} else if !bytes.Equal(fullPath, pathSeparator) {
|
||||
fullPath = append(fullPath, pathSeparator...)
|
||||
}
|
||||
}
|
||||
fullPath = append(fullPath, name...)
|
||||
//logging.LogInfo("getting full path", "fullPath", fullPath, "parentPath", parentPath, "name", name)
|
||||
if len(fullPath) > 1 {
|
||||
if fullPath[len(fullPath)-1] == pathSeparator[len(pathSeparator)-1] {
|
||||
fullPath = fullPath[:len(fullPath)-1]
|
||||
}
|
||||
}
|
||||
return fullPath
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -2213,6 +2269,9 @@ func getParentPathFullPathName(pathData utils.AnalyzedPath, endIndex int, treeTy
|
||||
logging.LogError(nil, "can't get parent full path name when end index < 0")
|
||||
return "", "", ""
|
||||
}
|
||||
if len(pathData.PathPieces) == 0 {
|
||||
return "", "", ""
|
||||
}
|
||||
switch treeType {
|
||||
case databaseStructs.TREE_TYPE_FILE:
|
||||
if endIndex == 0 {
|
||||
@@ -2241,6 +2300,35 @@ func getParentPathFullPathName(pathData utils.AnalyzedPath, endIndex int, treeTy
|
||||
//logging.LogInfo("getting path info", "parentPath", parentPath, "fullPath", fullPath, "name", name)
|
||||
return parentPath, fullPath, name
|
||||
default:
|
||||
custom, ok := getCustomBrowser(treeType)
|
||||
if ok && custom.Type == databaseStructs.TREE_TYPE_FILE {
|
||||
if pathData.ReverseCombine {
|
||||
endIndex = len(pathData.PathPieces) - 1 - endIndex
|
||||
if endIndex == len(pathData.PathPieces)-1 {
|
||||
// this is the root node, so we're looking for name=name, parent_path="", full_path=name
|
||||
fullPath = pathData.PathPieces[endIndex]
|
||||
parentPath = ""
|
||||
name = pathData.PathPieces[endIndex]
|
||||
} else {
|
||||
parentPath = strings.Join(pathData.PathPieces[endIndex+1:], pathData.PathSeparator)
|
||||
fullPath = strings.Join(pathData.PathPieces[endIndex:], pathData.PathSeparator)
|
||||
name = pathData.PathPieces[endIndex]
|
||||
}
|
||||
} else {
|
||||
if endIndex == 0 {
|
||||
// this is the root node, so we're looking for name=name, parent_path="", full_path=name
|
||||
fullPath = pathData.PathPieces[endIndex]
|
||||
parentPath = ""
|
||||
name = pathData.PathPieces[endIndex]
|
||||
} else {
|
||||
parentPath = strings.Join(pathData.PathPieces[:endIndex], pathData.PathSeparator)
|
||||
fullPath = strings.Join(pathData.PathPieces[:endIndex+1], pathData.PathSeparator)
|
||||
name = pathData.PathPieces[endIndex]
|
||||
}
|
||||
}
|
||||
//logging.LogInfo("getting path info", "parentPath", parentPath, "fullPath", fullPath, "name", name)
|
||||
return parentPath, fullPath, name
|
||||
}
|
||||
logging.LogError(nil, "Unknown mythictree type", "tree type", treeType)
|
||||
return "", "", ""
|
||||
}
|
||||
@@ -2289,13 +2377,15 @@ func createTreeNode(treeNode *databaseStructs.MythicTree) {
|
||||
}
|
||||
|
||||
statement, err := database.DB.PrepareNamed(`INSERT INTO mythictree
|
||||
(host, task_id, operation_id, "name", full_path, parent_path, tree_type, can_have_children, success, metadata, os, callback_id, apitokens_id)
|
||||
(host, task_id, operation_id, "name", full_path, parent_path, tree_type, can_have_children, success, metadata, os, callback_id, apitokens_id, has_children, display_path)
|
||||
VALUES
|
||||
(:host, :task_id, :operation_id, :name, :full_path, :parent_path, :tree_type, :can_have_children, :success, :metadata, :os, :callback_id, :apitokens_id)
|
||||
(:host, :task_id, :operation_id, :name, :full_path, :parent_path, :tree_type, :can_have_children, :success, :metadata, :os, :callback_id, :apitokens_id, :has_children, :display_path)
|
||||
ON CONFLICT (host, operation_id, full_path, tree_type, callback_id)
|
||||
DO UPDATE SET
|
||||
task_id=:task_id, "name"=:name, parent_path=:parent_path, can_have_children=:can_have_children,
|
||||
metadata=mythictree.metadata || :metadata, os=:os, "timestamp"=now(), deleted=false
|
||||
task_id=:task_id, "name"=:name, parent_path=:parent_path,
|
||||
can_have_children=(mythictree.can_have_children OR :can_have_children),
|
||||
has_children=(mythictree.has_children OR :has_children),
|
||||
metadata=mythictree.metadata || :metadata, os=:os, "timestamp"=now(), deleted=false, display_path=:display_path
|
||||
RETURNING id`)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to create or update mythictree statement")
|
||||
@@ -2320,8 +2410,8 @@ func addFileMetaToMythicTree(task databaseStructs.Task, newFile databaseStructs.
|
||||
return
|
||||
}
|
||||
err := database.DB.Get(&fileBrowser, `SELECT id FROM mythictree WHERE
|
||||
operation_id=$1 AND full_path=$2 AND tree_type='file' AND callback_id=$3`,
|
||||
newFile.OperationID, newFile.FullRemotePath, task.Callback.ID)
|
||||
operation_id=$1 AND full_path=$2 AND tree_type='file' AND callback_id=$3 AND host=$4`,
|
||||
newFile.OperationID, newFile.FullRemotePath, task.Callback.ID, newFile.Host)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
pathData, err := utils.SplitFilePathGetHost(string(newFile.FullRemotePath), "", []string{})
|
||||
if err != nil {
|
||||
@@ -2330,26 +2420,9 @@ func addFileMetaToMythicTree(task databaseStructs.Task, newFile databaseStructs.
|
||||
return
|
||||
}
|
||||
if pathData.Host == "" {
|
||||
pathData.Host = strings.ToUpper(task.Callback.Host)
|
||||
pathData.Host = newFile.Host
|
||||
}
|
||||
associateFileMetaWithMythicTree(pathData, newFile, task)
|
||||
/*
|
||||
resolveAndCreateParentPathsForTreeNode(pathData, task, databaseStructs.TREE_TYPE_FILE)
|
||||
if err := database.DB.Get(&fileBrowser, `SELECT id FROM mythictree WHERE
|
||||
operation_id=$1 AND full_path=$2 AND tree_type='file' AND host=$3 AND callback_id=$4`,
|
||||
newFile.OperationID, newFile.FullRemotePath, newFile.Host, task.Callback.ID); err != nil {
|
||||
logging.LogError(err, "Failed to find, create, and then fine mythic tree data for newly downloaded file")
|
||||
} else {
|
||||
newFile.MythicTreeID.Int64 = int64(fileBrowser.ID)
|
||||
newFile.MythicTreeID.Valid = true
|
||||
if _, err := database.DB.NamedExec(`UPDATE filemeta SET mythictree_id=:mythictree_id WHERE id=:id`, newFile); err != nil {
|
||||
logging.LogError(err, "Failed to update file meta with mythic tree id")
|
||||
} else if _, err := database.DB.Exec(`UPDATE mythictree SET can_have_children=false WHERE id=$1`, fileBrowser.ID); err != nil {
|
||||
logging.LogError(err, "Failed to update browser object to file instead of folder")
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
} else if err == nil {
|
||||
newFile.MythicTreeID.Int64 = int64(fileBrowser.ID)
|
||||
newFile.MythicTreeID.Valid = true
|
||||
@@ -2439,3 +2512,248 @@ func submitAgentAlertToWebhook(operationID int, callbackID int, callbackDisplayI
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type customBrowser struct {
|
||||
// Name is the name of the custom browser, like ldap, registry, or agent specific like apollo_ldap
|
||||
Name string `json:"name"`
|
||||
// Separator is the type of path seperator
|
||||
Separator string `json:"separator"`
|
||||
// Type indicates if this is a File or Process based hierarchy
|
||||
Type string `json:"type"`
|
||||
//
|
||||
Os string `json:"os"`
|
||||
}
|
||||
|
||||
var customBrowserCache = make(map[string]customBrowser)
|
||||
var customBrowserCacheMutex sync.RWMutex
|
||||
|
||||
func updateCustomBrowserCache() {
|
||||
customBrowserCacheMutex.Lock()
|
||||
customBrowserCache = make(map[string]customBrowser)
|
||||
customBrowserCacheMutex.Unlock()
|
||||
}
|
||||
func getCustomBrowser(name string) (customBrowser, bool) {
|
||||
customBrowserCacheMutex.RLock()
|
||||
browser, ok := customBrowserCache[name]
|
||||
customBrowserCacheMutex.RUnlock()
|
||||
if ok {
|
||||
return browser, ok
|
||||
}
|
||||
// didn't find the browser, so try fetching it from the database and caching it
|
||||
// fake this for now
|
||||
|
||||
databaseBrowser := databaseStructs.CustomBrowser{
|
||||
Name: name,
|
||||
}
|
||||
customBrowserCacheMutex.Lock()
|
||||
err := database.DB.Get(&databaseBrowser, `SELECT * FROM custombrowser WHERE "name" = $1`, name)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to find custom browser")
|
||||
customBrowserCacheMutex.Unlock()
|
||||
return customBrowser{}, false
|
||||
}
|
||||
customBrowserCache[name] = customBrowser{
|
||||
Name: name,
|
||||
Separator: databaseBrowser.Separator,
|
||||
Type: databaseBrowser.Type,
|
||||
}
|
||||
browser, ok = customBrowserCache[name]
|
||||
customBrowserCacheMutex.Unlock()
|
||||
return browser, true
|
||||
}
|
||||
func handleAgentMessagePostResponseCustomBrowser(task databaseStructs.Task, agentCustomBrowser *agentMessagePostResponseCustomBrowser) {
|
||||
if agentCustomBrowser.BrowserName == "" {
|
||||
return
|
||||
}
|
||||
// given a custom browser object, need to insert it into database and potentially insert parents along the way
|
||||
if agentCustomBrowser.SetAsUserOutput {
|
||||
go func(fileBrowserForOutput *agentMessagePostResponseCustomBrowser) {
|
||||
outputBytes, err := json.Marshal(fileBrowserForOutput)
|
||||
if err != nil {
|
||||
logging.LogError(err, "failed to marshal filebrowser data to JSON")
|
||||
return
|
||||
}
|
||||
asyncAgentMessagePostResponseChannel <- agentAgentMessagePostResponseChannelMessage{
|
||||
Task: task,
|
||||
Response: string(outputBytes),
|
||||
SequenceNum: nil,
|
||||
}
|
||||
}(agentCustomBrowser)
|
||||
}
|
||||
if agentCustomBrowser.Entries == nil || len(*agentCustomBrowser.Entries) == 0 {
|
||||
return
|
||||
}
|
||||
customBrowserData, ok := getCustomBrowser(agentCustomBrowser.BrowserName)
|
||||
if !ok {
|
||||
logging.LogError(errors.New("unknown custom browser"), "Failed to get custom browser", "browser", agentCustomBrowser.BrowserName)
|
||||
return
|
||||
}
|
||||
for _, entry := range *agentCustomBrowser.Entries {
|
||||
pathData, err := utils.SplitCustomPath(entry.ParentPath, entry.Name, customBrowserData.Separator)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to add data for custom browser due to path issue")
|
||||
go SendAllOperationsMessage(err.Error(), task.OperationID, "", database.MESSAGE_LEVEL_AGENT_MESSGAGE, true)
|
||||
continue
|
||||
}
|
||||
pathData.Host = agentCustomBrowser.Host
|
||||
if pathData.Host == "" {
|
||||
pathData.Host = strings.ToUpper(task.Callback.Host)
|
||||
} else {
|
||||
pathData.Host = strings.ToUpper(pathData.Host)
|
||||
}
|
||||
displayData, err := utils.SplitCustomPath(entry.DisplayPath, entry.Name, customBrowserData.Separator)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to add data for custom browser due to display path issue")
|
||||
displayData = utils.AnalyzedPath{}
|
||||
} else if len(displayData.PathPieces) > 0 && displayData.PathPieces[0] == entry.Name {
|
||||
// we're looking at display data that's reversed, so track that
|
||||
displayData.PathPieces = displayData.PathPieces[1:]
|
||||
displayData.ReverseCombine = true
|
||||
}
|
||||
logging.LogInfo("SplitCustomPathResponse", "pathData", pathData)
|
||||
resolveAndCreateParentPathsForTreeNode(pathData, displayData, task, customBrowserData.Name)
|
||||
// now that the parents and all ancestors are resolved, process the current path and all children
|
||||
realParentPath := strings.Join(pathData.PathPieces, pathData.PathSeparator)
|
||||
if entry.Name == "" {
|
||||
logging.LogError(nil, "Can't create browser entry with empty name")
|
||||
continue
|
||||
}
|
||||
fullPath := treeNodeGetFullPath(
|
||||
[]byte(realParentPath),
|
||||
[]byte(entry.Name),
|
||||
[]byte(pathData.PathSeparator),
|
||||
customBrowserData.Name)
|
||||
parentPath := treeNodeGetFullPath([]byte(realParentPath), []byte(""), []byte(pathData.PathSeparator), customBrowserData.Name)
|
||||
name := treeNodeGetFullPath([]byte(""), []byte(entry.Name), []byte(pathData.PathSeparator), customBrowserData.Name)
|
||||
logging.LogInfo("creating entry", "name", entry.Name, "parentPath", realParentPath, "fullPath", fullPath)
|
||||
newTree := databaseStructs.MythicTree{
|
||||
Host: pathData.Host,
|
||||
TaskID: task.ID,
|
||||
OperationID: task.OperationID,
|
||||
Name: name,
|
||||
ParentPath: parentPath,
|
||||
FullPath: fullPath,
|
||||
TreeType: customBrowserData.Name,
|
||||
CanHaveChildren: entry.CanHaveChildren,
|
||||
Deleted: false,
|
||||
Os: getOSTypeBasedOnPathSeparator(pathData.PathSeparator, customBrowserData.Name),
|
||||
DisplayPath: []byte(entry.DisplayPath),
|
||||
}
|
||||
if entry.Success != nil {
|
||||
newTree.Success.Valid = true
|
||||
newTree.Success.Bool = *entry.Success
|
||||
}
|
||||
if entry.Children != nil && len(*entry.Children) > 0 {
|
||||
newTree.HasChildren = true
|
||||
}
|
||||
newTree.Metadata = GetMythicJSONTextFromStruct(entry.Metadata)
|
||||
newTree.CallbackID.Valid = true
|
||||
newTree.CallbackID.Int64 = int64(task.Callback.ID)
|
||||
createTreeNode(&newTree)
|
||||
if agentCustomBrowser.UpdateDeleted {
|
||||
if customBrowserData.Type == databaseStructs.TREE_TYPE_FILE {
|
||||
// we need to iterate over the children for this entry and potentially remove any that the database know of but that aren't in our `files` list
|
||||
var existingTreeEntries []databaseStructs.MythicTree
|
||||
err = database.DB.Select(&existingTreeEntries, `SELECT
|
||||
id, "name", success, full_path, parent_path, operation_id, host, tree_type
|
||||
FROM mythictree WHERE
|
||||
parent_path=$1 AND operation_id=$2 AND host=$3 AND tree_type=$4`,
|
||||
fullPath, task.OperationID, pathData.Host, customBrowserData.Name)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to fetch existing children when trying to update deleted")
|
||||
continue
|
||||
}
|
||||
var namesToDeleteAndUpdate []string // will get existing database IDs for things that aren't in the files list
|
||||
for _, existingEntry := range existingTreeEntries {
|
||||
if entry.Children != nil {
|
||||
existingEntryStillExists := false
|
||||
//logging.LogInfo("checking for file existing", "name", existingEntry.Name)
|
||||
for _, newEntry := range *entry.Children {
|
||||
if bytes.Equal([]byte(newEntry.Name), existingEntry.Name) {
|
||||
//logging.LogInfo("[+] found a match in existing data and new data", "name", newEntry.Name, "id", existingEntry.ID)
|
||||
namesToDeleteAndUpdate = append(namesToDeleteAndUpdate, newEntry.Name)
|
||||
existingEntryStillExists = true
|
||||
// update the entry in the database
|
||||
newTreeChild := databaseStructs.MythicTree{
|
||||
Host: pathData.Host,
|
||||
TaskID: task.ID,
|
||||
OperationID: task.OperationID,
|
||||
Name: []byte(newEntry.Name),
|
||||
ParentPath: existingEntry.ParentPath,
|
||||
FullPath: existingEntry.FullPath,
|
||||
TreeType: customBrowserData.Name,
|
||||
CanHaveChildren: newEntry.CanHaveChildren,
|
||||
DisplayPath: []byte(newEntry.DisplayPath),
|
||||
Deleted: false,
|
||||
Success: existingEntry.Success,
|
||||
ID: existingEntry.ID,
|
||||
Os: newTree.Os,
|
||||
}
|
||||
newTreeChild.Metadata = GetMythicJSONTextFromStruct(newEntry.Metadata)
|
||||
newTreeChild.CallbackID.Valid = true
|
||||
newTreeChild.CallbackID.Int64 = int64(task.Callback.ID)
|
||||
updateTreeNode(newTreeChild)
|
||||
}
|
||||
}
|
||||
if !existingEntryStillExists {
|
||||
//logging.LogError(nil, "failed to find match, marking as deleted", "name", existingEntry.Name)
|
||||
namesToDeleteAndUpdate = append(namesToDeleteAndUpdate, string(existingEntry.Name))
|
||||
existingEntry.Deleted = true
|
||||
existingEntry.TaskID = task.ID
|
||||
deleteTreeNode(existingEntry, true)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// now all existing ones have been updated or deleted, so it's time to add new ones
|
||||
if entry.Children != nil {
|
||||
for _, newEntry := range *entry.Children {
|
||||
if !utils.SliceContains(namesToDeleteAndUpdate, newEntry.Name) {
|
||||
// this isn't marked as updated or deleted, so let's create it
|
||||
newTreeChild := databaseStructs.MythicTree{
|
||||
Host: pathData.Host,
|
||||
TaskID: task.ID,
|
||||
OperationID: task.OperationID,
|
||||
Name: []byte(newEntry.Name),
|
||||
DisplayPath: []byte(newEntry.DisplayPath),
|
||||
ParentPath: fullPath,
|
||||
TreeType: customBrowserData.Name,
|
||||
CanHaveChildren: newEntry.CanHaveChildren,
|
||||
Deleted: false,
|
||||
Os: newTree.Os,
|
||||
}
|
||||
newTreeChild.FullPath = treeNodeGetFullPath(fullPath, []byte(newEntry.Name), []byte(pathData.PathSeparator), customBrowserData.Name)
|
||||
newTreeChild.Metadata = GetMythicJSONTextFromStruct(newEntry.Metadata)
|
||||
newTreeChild.CallbackID.Valid = true
|
||||
newTreeChild.CallbackID.Int64 = int64(task.Callback.ID)
|
||||
createTreeNode(&newTreeChild)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if entry.Children != nil {
|
||||
// we're not automatically updating deleted children, so just iterate over the files and insert/update them
|
||||
for _, newEntry := range *entry.Children {
|
||||
newTreeChild := databaseStructs.MythicTree{
|
||||
Host: pathData.Host,
|
||||
TaskID: task.ID,
|
||||
OperationID: task.OperationID,
|
||||
Name: []byte(newEntry.Name),
|
||||
DisplayPath: []byte(newEntry.DisplayPath),
|
||||
ParentPath: fullPath,
|
||||
TreeType: customBrowserData.Name,
|
||||
CanHaveChildren: newEntry.CanHaveChildren,
|
||||
Deleted: false,
|
||||
Os: newTree.Os,
|
||||
}
|
||||
newTreeChild.FullPath = treeNodeGetFullPath(fullPath, []byte(newEntry.Name), []byte(pathData.PathSeparator), customBrowserData.Name)
|
||||
newTreeChild.Metadata = GetMythicJSONTextFromStruct(newEntry.Metadata)
|
||||
newTreeChild.CallbackID.Valid = true
|
||||
newTreeChild.CallbackID.Int64 = int64(task.Callback.ID)
|
||||
logging.LogInfo("creating entry child", "name", newEntry.Name, "parentPath", fullPath)
|
||||
createTreeNode(&newTreeChild)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package rabbitmq
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/its-a-feature/Mythic/database"
|
||||
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
|
||||
"github.com/its-a-feature/Mythic/logging"
|
||||
@@ -24,10 +25,10 @@ func handleAgentMessageStagingTranslation(incoming *map[string]interface{}, uUID
|
||||
/*
|
||||
# message back should have:
|
||||
# {
|
||||
# "action": "staging_translator",
|
||||
# "action": "staging_translation",
|
||||
# "session_id": "a string way if needed to tell two concurrently staging requests apart"
|
||||
# "enc_key": raw bytes of the encryption key to use for the next request
|
||||
# "dec_key": raw bytes of the decryption key to use for the next request
|
||||
# "enc_key": base64 of the encryption key to use for the next request
|
||||
# "dec_key": base64 of the decryption key to use for the next request
|
||||
# "type": "string crypto type"
|
||||
# "next_uuid": "string UUID that will be used for the next message to pull this information back from the database",
|
||||
# "message": the final message you want to actually go back to the agent
|
||||
|
||||
@@ -149,6 +149,9 @@ func GetAuthContainerGetNonIDPMetadataRoutingKey(container string) string {
|
||||
func GetContainerOnStartRoutingKey(container string) string {
|
||||
return fmt.Sprintf("%s_%s", container, CONTAINER_ON_START)
|
||||
}
|
||||
func GetCustomBrowserExportFunctionRoutingKey(container string) string {
|
||||
return fmt.Sprintf("%s_%s", container, CUSTOMBROWSER_EXPORT_FUNCTION)
|
||||
}
|
||||
|
||||
func (r *rabbitMQConnection) GetConnection() (*amqp.Connection, error) {
|
||||
// use a mutex lock around getting the connection because we don't want to accidentally have leaking connections
|
||||
@@ -270,10 +273,10 @@ func (r *rabbitMQConnection) SendMessage(exchange string, queue string, correlat
|
||||
if !ignoreErrormessage {
|
||||
logging.LogError(errors.New("failed 3 times"), "failed 3 times", "queue", queue)
|
||||
}
|
||||
return errors.New("failed 3 times")
|
||||
return errors.New(fmt.Sprintf("failed 3 times to send to queue %s", queue))
|
||||
}
|
||||
func (r *rabbitMQConnection) SendRPCMessage(exchange string, queue string, body []byte, exclusiveQueue bool) ([]byte, error) {
|
||||
var err error
|
||||
var finalError error
|
||||
for attempt := 0; attempt < 3; attempt++ {
|
||||
conn, err := r.GetConnection()
|
||||
if err != nil {
|
||||
@@ -333,6 +336,7 @@ func (r *rabbitMQConnection) SendRPCMessage(exchange string, queue string, body
|
||||
false, // immediate
|
||||
msg, // publishing
|
||||
)
|
||||
finalError = err
|
||||
if err != nil {
|
||||
logging.LogError(err, "there was an error publishing an rpc message", "queue", queue)
|
||||
ch.Close()
|
||||
@@ -342,7 +346,8 @@ func (r *rabbitMQConnection) SendRPCMessage(exchange string, queue string, body
|
||||
select {
|
||||
case ntf := <-confirmChannel:
|
||||
if !ntf.Ack {
|
||||
err := errors.New("failed to deliver message, not ACK-ed by receiver")
|
||||
err = errors.New("failed to deliver message, not ACK-ed by receiver")
|
||||
finalError = err
|
||||
logging.LogError(err, "failed to deliver message to exchange/queue, notifyPublish", "queue", queue)
|
||||
ch.Close()
|
||||
time.Sleep(RPC_TIMEOUT)
|
||||
@@ -350,11 +355,13 @@ func (r *rabbitMQConnection) SendRPCMessage(exchange string, queue string, body
|
||||
}
|
||||
case ret := <-notifyReturnChannel:
|
||||
err = errors.New(getMeaningfulRabbitmqError(ret))
|
||||
finalError = err
|
||||
time.Sleep(RPC_TIMEOUT)
|
||||
ch.Close()
|
||||
continue
|
||||
case <-time.After(RPC_TIMEOUT):
|
||||
err = errors.New("message delivery confirmation timed out in SendRPCMessage")
|
||||
finalError = err
|
||||
logging.LogError(err, "message delivery confirmation to exchange/queue timed out when sending", "queue", queue)
|
||||
ch.Close()
|
||||
continue
|
||||
@@ -367,13 +374,14 @@ func (r *rabbitMQConnection) SendRPCMessage(exchange string, queue string, body
|
||||
return m.Body, nil
|
||||
case <-time.After(RPC_TIMEOUT):
|
||||
err = errors.New("message delivery confirmation timed out")
|
||||
finalError = err
|
||||
logging.LogError(err, "message delivery confirmation to exchange/queue timed out when receiving", "queue", queue)
|
||||
ch.Close()
|
||||
continue
|
||||
}
|
||||
}
|
||||
logging.LogError(err, "failed 3 times")
|
||||
return nil, err
|
||||
logging.LogError(finalError, "failed 3 times")
|
||||
return nil, finalError
|
||||
}
|
||||
func (r *rabbitMQConnection) ReceiveFromMythicDirectExchange(exchange string, queue string, routingKey string, handler QueueHandler, exclusiveQueue bool) {
|
||||
// exchange is a direct exchange
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const mythicServerVersion = "3.4.10"
|
||||
const mythicServerVersion = "3.4.11"
|
||||
|
||||
type Config struct {
|
||||
// server configuration
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user