custom browser update

This commit is contained in:
its-a-feature
2025-12-10 08:20:43 -06:00
parent e7405bd62c
commit 36ceaaacba
115 changed files with 6907 additions and 807 deletions
+31
View File
@@ -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
+46 -1
View File
@@ -130,4 +130,49 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.
MIT license
===========
Copyright (c) 2017 sql.js authors (see AUTHORS)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# Some portions of the Makefile taken from:
Copyright 2017 Ryusei Yamaguchi
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+26
View File
@@ -0,0 +1,26 @@
const webpack = require('webpack');
module.exports = function override(config, env) {
config.resolve.fallback = {
//url: require.resolve('url'),
fs: false,
assert: require.resolve('assert'),
crypto: require.resolve('crypto-browserify'),
path: require.resolve('path-browserify'),
//http: require.resolve('stream-http'),
//https: require.resolve('https-browserify'),
//os: require.resolve('os-browserify/browser'),
//buffer: require.resolve('buffer'),
stream: require.resolve('stream-browserify'),
};
/*
config.plugins.push(
new webpack.ProvidePlugin({
process: 'process/browser',
Buffer: ['buffer', 'Buffer'],
}),
);
*/
return config;
}
+697 -1
View File
@@ -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",
+11 -5
View File
@@ -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": [
+11 -2
View File
@@ -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 = [
+21 -3
View File
@@ -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;
@@ -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>
);
@@ -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':
@@ -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>
@@ -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>
);
}
@@ -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} />
)
}
+1 -1
View File
@@ -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;
+6 -10
View File
@@ -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};
+7
View File
@@ -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
+7 -2
View File
@@ -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.
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -1 +1 @@
3.4.10
3.4.11
+13
View File
@@ -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
}
+11
View File
@@ -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: []
+171
View File
@@ -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
+1 -1
View File
@@ -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"`
}
+22 -22
View File
@@ -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"`
}
+4 -2
View File
@@ -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 -1
View File
@@ -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"
)
+11 -3
View File
@@ -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
+45 -18
View File
@@ -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")
}
}
}
+2 -1
View File
@@ -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
+1 -1
View File
@@ -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