mirror of
https://github.com/its-a-feature/Mythic.git
synced 2026-11-08 08:08:01 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96b24b48a7 | ||
|
|
bdc722f6ce | ||
|
|
f9e71d0859 | ||
|
|
742d878928 | ||
|
|
01ea8bc896 | ||
|
|
94418bcd52 | ||
|
|
3ae9c5fe18 | ||
|
|
5e85a20d14 | ||
|
|
5f46db6ede | ||
|
|
e37e057a89 | ||
|
|
33a46d7db7 | ||
|
|
540aec0f00 | ||
|
|
29be09eef4 | ||
|
|
a5a1a972d7 | ||
|
|
82648e8241 | ||
|
|
e4da2c887f | ||
|
|
39f44f7dec | ||
|
|
0f51584493 | ||
|
|
dc50ecc0dc | ||
|
|
59211a2fbf | ||
|
|
f8d355a29c | ||
|
|
1da8fdcd2a | ||
|
|
2dcc3b5699 | ||
|
|
8deec59270 | ||
|
|
071abeaf74 | ||
|
|
6eb53da6b5 | ||
|
|
d605a2f82c | ||
|
|
31db999945 | ||
|
|
9ae193be76 | ||
|
|
fc731af819 | ||
|
|
d21dbf293b | ||
|
|
b6d98b1794 | ||
|
|
448a57b82b | ||
|
|
1d54d5ea0c | ||
|
|
4d793d5998 | ||
|
|
60455c26cb | ||
|
|
183f0605c2 | ||
|
|
37d3cc8b1b | ||
|
|
9532b96f7a | ||
|
|
e1806f0fd5 | ||
|
|
fa235b6378 | ||
|
|
b87219b1e9 | ||
|
|
b5459b56c8 | ||
|
|
ca6d7d2ce0 | ||
|
|
6148c5f0fb | ||
|
|
6e14b2a472 | ||
|
|
307df57df8 | ||
|
|
4a4d95f1ca | ||
|
|
ffc5041349 | ||
|
|
ecda245963 | ||
|
|
6ea67c69b9 | ||
|
|
64d32eb116 | ||
|
|
9f7a1ce770 | ||
|
|
4a3818988c | ||
|
|
1ec6318b62 | ||
|
|
60902a6e66 | ||
|
|
ddda1a266c | ||
|
|
8dc9f351d9 | ||
|
|
d7b4d51186 | ||
|
|
d2334b79de | ||
|
|
96df4db36e | ||
|
|
a72dd1b086 | ||
|
|
89bee21894 | ||
|
|
26a4410f0a | ||
|
|
36fb4c58bf | ||
|
|
cfdf0c537d | ||
|
|
f23e123a56 | ||
|
|
5b4c210e6f | ||
|
|
3dee8797da | ||
|
|
33f48baab4 | ||
|
|
3198f304c3 | ||
|
|
09ac733d45 | ||
|
|
3e611494cc | ||
|
|
8be3c084e5 | ||
|
|
3796640835 | ||
|
|
6e23b29f22 | ||
|
|
22e5c489b2 | ||
|
|
8cdf34c3e7 | ||
|
|
b285558fce | ||
|
|
e536f59347 | ||
|
|
4c472f7e3a | ||
|
|
cc77f2b1a3 | ||
|
|
1c18703e96 | ||
|
|
f017661b3d | ||
|
|
98dd30d984 | ||
|
|
eb3cf1053b | ||
|
|
58e2c6de5f | ||
|
|
7dc66c43aa | ||
|
|
9c6afe2d8c | ||
|
|
6c5b1d2a24 | ||
|
|
fc8239eb85 | ||
|
|
180c6e1914 | ||
|
|
59c97d791f | ||
|
|
d9e1d5e6bd | ||
|
|
0f5746b4a3 | ||
|
|
71c03251fb | ||
|
|
f742ab3d0f | ||
|
|
d3ae12a220 | ||
|
|
d9a41572ac | ||
|
|
e454531999 | ||
|
|
45cf6a9cd8 | ||
|
|
5011314ba5 | ||
|
|
af9d3c40e6 | ||
|
|
03e27abdb6 | ||
|
|
e7fde3fcde | ||
|
|
51c0eeb5df | ||
|
|
0f259d1f7d | ||
|
|
d86f0d6227 | ||
|
|
3bb93efa72 | ||
|
|
652234d7e1 | ||
|
|
44d8fd168e | ||
|
|
2e418bbb4d | ||
|
|
87249b2891 | ||
|
|
dbb15fdbb3 | ||
|
|
04ebb6aba7 | ||
|
|
8a2e42c48c | ||
|
|
db682d5020 | ||
|
|
6649279e3e | ||
|
|
6d1778bb3b | ||
|
|
0484154eae | ||
|
|
99f2b6cb2f | ||
|
|
303741ee28 | ||
|
|
37a3f66d68 | ||
|
|
8e43275c27 | ||
|
|
6dfe40b274 | ||
|
|
49e1c62502 | ||
|
|
a739837c95 | ||
|
|
063f70a55d | ||
|
|
fb6c3d8368 | ||
|
|
10f0eb9e7a | ||
|
|
afdd0ec9cc | ||
|
|
2391244baa | ||
|
|
9e0fbcfa3a | ||
|
|
54e6cc5d32 | ||
|
|
c56d92ee52 | ||
|
|
706e8d3ce7 | ||
|
|
29e57f1384 | ||
|
|
bf7de9ad22 | ||
|
|
e3cbdd35e7 | ||
|
|
2ca4ad507e | ||
|
|
43a8a8580c | ||
|
|
fd02fe2c9e | ||
|
|
3a6d9c4842 | ||
|
|
31e297a37f | ||
|
|
b40aed02b8 | ||
|
|
5e031f64b2 | ||
|
|
f6532b127d | ||
|
|
a383fcf8c5 | ||
|
|
307eb0dd72 | ||
|
|
29db86be7c | ||
|
|
04be0065f3 | ||
|
|
8601357379 | ||
|
|
bb3a4b2294 | ||
|
|
44149bdf2b | ||
|
|
1a2bf56c8c | ||
|
|
63867bc27a | ||
|
|
42a7869750 | ||
|
|
8479a2d091 | ||
|
|
c7357c6fee | ||
|
|
e6fd09b069 | ||
|
|
8cc47d518e | ||
|
|
ae2ed75801 | ||
|
|
7f225e51f1 | ||
|
|
c173f083dd | ||
|
|
3e3356e586 | ||
|
|
70a4ca26ec | ||
|
|
64fd83b999 | ||
|
|
180a81111d | ||
|
|
767881fd6c | ||
|
|
4240722b3a | ||
|
|
36ceaaacba | ||
|
|
bf22f85661 | ||
|
|
e7405bd62c | ||
|
|
2ab6e20162 | ||
|
|
274e41f1ee | ||
|
|
3748d68f2e | ||
|
|
e8efb8acea | ||
|
|
438bbf1b3d | ||
|
|
5812391061 | ||
|
|
07e6bedf71 | ||
|
|
1966848577 | ||
|
|
3953850496 | ||
|
|
aa1de9ef14 | ||
|
|
7dd545fb9d | ||
|
|
c997ab3a8b | ||
|
|
7e95ae879d | ||
|
|
5d4c173099 | ||
|
|
8095eee496 | ||
|
|
9ed4a8ee49 | ||
|
|
520e3a94ca | ||
|
|
9a44032528 | ||
|
|
df8b903757 | ||
|
|
3a814eca14 | ||
|
|
d169ee834e | ||
|
|
c7dab9a565 | ||
|
|
1e0202eaba | ||
|
|
405f58c68e | ||
|
|
ba593a8f94 | ||
|
|
ebf6425fd9 | ||
|
|
8903cf243a | ||
|
|
6333fef797 | ||
|
|
cf557df672 | ||
|
|
c2bbc061bf | ||
|
|
af5f4416a6 | ||
|
|
0692cad336 | ||
|
|
e244f9fcd1 | ||
|
|
dd8c548242 | ||
|
|
137b5cc4a1 | ||
|
|
92f3ee46b6 | ||
|
|
8979bd1f42 | ||
|
|
46abe5a032 | ||
|
|
b294c8ff53 | ||
|
|
11ab25a74e | ||
|
|
33d0e0b7a3 | ||
|
|
48c77ed20b | ||
|
|
64828ca26e | ||
|
|
96231b6f5e | ||
|
|
f1c1ceca91 | ||
|
|
2826b39c90 | ||
|
|
14e5a10a9a | ||
|
|
675827ecce | ||
|
|
599cb57682 | ||
|
|
2bd5da93ff | ||
|
|
4896194b0c | ||
|
|
fae956bec4 | ||
|
|
09e0b011f1 | ||
|
|
62c60601c3 | ||
|
|
461c6c689e | ||
|
|
b7d113753f | ||
|
|
1f274b0697 | ||
|
|
c52a6c66e9 | ||
|
|
74864c1803 | ||
|
|
364f87a6f2 | ||
|
|
a1094f6076 | ||
|
|
f898a09c83 | ||
|
|
c416292714 | ||
|
|
759ab426b6 | ||
|
|
cd3e5ed774 | ||
|
|
ffafcd86b9 | ||
|
|
7e9394e070 | ||
|
|
2a6bd6e50b | ||
|
|
c0f7b94b2f | ||
|
|
62c1151c47 | ||
|
|
8deb1d9913 | ||
|
|
084ab87593 | ||
|
|
977f796cb1 | ||
|
|
07722b7de5 | ||
|
|
b5cac5d3f1 | ||
|
|
da84bf5a47 | ||
|
|
ab7f539ed2 | ||
|
|
ae2d6e8dbd | ||
|
|
1b7fd846a2 | ||
|
|
1e34aeaacc | ||
|
|
8e9ca5b14a | ||
|
|
5338775d02 | ||
|
|
81c260d4da | ||
|
|
62f3c4c9c6 | ||
|
|
ee7441c599 | ||
|
|
3363e3d2f2 | ||
|
|
765018ab74 | ||
|
|
d36774e0b8 | ||
|
|
56bcba9b8d | ||
|
|
ae8f98cf0d | ||
|
|
09871f8ba3 | ||
|
|
02db11e522 | ||
|
|
db090b63ce | ||
|
|
4769c29ea1 | ||
|
|
a6cb5064c5 | ||
|
|
a725e36443 | ||
|
|
0ffbe0f0a5 | ||
|
|
dc146b8988 | ||
|
|
77166f400b | ||
|
|
7dd02aedb4 | ||
|
|
6fbf73ceb7 | ||
|
|
a13b8c88c4 | ||
|
|
b837684a79 | ||
|
|
de2d6080e9 | ||
|
|
775156efdc | ||
|
|
5d5e40369b | ||
|
|
ac353c3cca | ||
|
|
a4d61b5e0e | ||
|
|
2b4eb73bd5 | ||
|
|
b527bf7e0c | ||
|
|
4e8eecfe25 | ||
|
|
faf32c60f1 | ||
|
|
0fcd35a039 | ||
|
|
367f954050 | ||
|
|
49101478cb | ||
|
|
68630fe297 | ||
|
|
dbdd60c6fb | ||
|
|
c841189f00 | ||
|
|
714aec37cc | ||
|
|
121695b5f1 | ||
|
|
fd64fa2ba4 | ||
|
|
bef32bc01b | ||
|
|
f75cb4f853 | ||
|
|
f8e71c493b | ||
|
|
471d74de44 | ||
|
|
8cdcd3168b | ||
|
|
677ccdcc14 | ||
|
|
23e857b940 | ||
|
|
612eca9061 | ||
|
|
c16b4d0db3 | ||
|
|
d6f454374a | ||
|
|
4649197c17 | ||
|
|
c3d2bdc328 | ||
|
|
8f980dcf51 | ||
|
|
a21deb9d41 | ||
|
|
f6535dd91e | ||
|
|
b62ea98c10 | ||
|
|
301efc044e | ||
|
|
5d0b3d1a94 | ||
|
|
25799523d0 | ||
|
|
83ef0f2b3a | ||
|
|
dba92abbc1 | ||
|
|
ec51eb4f90 | ||
|
|
fcc8398d55 | ||
|
|
ba75470a64 | ||
|
|
caa23d3927 | ||
|
|
7e4603ddec | ||
|
|
5004a5a043 | ||
|
|
e67b1c7339 | ||
|
|
6a4c154d75 | ||
|
|
d717a58082 | ||
|
|
982d7e1391 | ||
|
|
8b788ccc8c | ||
|
|
bb280887c6 | ||
|
|
7d402c4245 | ||
|
|
0ddfa50ce4 | ||
|
|
a694353648 | ||
|
|
fbab9d91f6 | ||
|
|
0c95773d21 | ||
|
|
1168f79794 | ||
|
|
6188c0fd1e | ||
|
|
ba1d98dae6 | ||
|
|
8e5b89f06c | ||
|
|
5927919dc4 | ||
|
|
7c7a545e86 | ||
|
|
d1443be757 | ||
|
|
ac49ee1d89 | ||
|
|
463167427f | ||
|
|
57085f500b | ||
|
|
8365c9e0bf | ||
|
|
31115f18fc | ||
|
|
83cf83edea | ||
|
|
b800a1dcb0 | ||
|
|
846a181bb4 | ||
|
|
a0517b6a52 | ||
|
|
c47d16f0d0 | ||
|
|
208d580d96 | ||
|
|
8ead31a150 | ||
|
|
fef2234bed | ||
|
|
9299cf9ef9 | ||
|
|
26decb421f | ||
|
|
4044c1346c | ||
|
|
0c887f4acf | ||
|
|
81934665ef | ||
|
|
5c48a51507 | ||
|
|
b295ebd3ae | ||
|
|
ab699c5685 | ||
|
|
dd9bef6daf | ||
|
|
19bb380f12 | ||
|
|
5aa4d3453f | ||
|
|
59862978de | ||
|
|
48e1ddb9a5 | ||
|
|
9d92b1644b | ||
|
|
04992aa1bc | ||
|
|
98eced6f79 | ||
|
|
ef6fb08cba | ||
|
|
bc9f3287b7 | ||
|
|
16fdecee28 | ||
|
|
4fad02e3c9 | ||
|
|
d3d6d4a2fb | ||
|
|
ce586d6980 | ||
|
|
69feb548e1 | ||
|
|
9290ca16af | ||
|
|
30e5c69dc4 | ||
|
|
ef7f39f19d | ||
|
|
6361d93672 | ||
|
|
855f633e74 | ||
|
|
26afca9280 | ||
|
|
a1d225612e | ||
|
|
80ecc5cfe2 | ||
|
|
b3dcc7d1fc | ||
|
|
166869f6c5 | ||
|
|
520995fab9 | ||
|
|
5878378db7 | ||
|
|
a819b51404 | ||
|
|
a906c1afcf | ||
|
|
b72ec32a92 | ||
|
|
e68be3891a | ||
|
|
340ecb1625 | ||
|
|
37fcbd0728 | ||
|
|
ef4637e5ed | ||
|
|
bbfcb19366 | ||
|
|
86c6488a89 | ||
|
|
1366ebed6c | ||
|
|
68c2005ff3 | ||
|
|
5f9c16099e | ||
|
|
71ca8d9122 | ||
|
|
2fb43a3314 | ||
|
|
0831e8c4e5 | ||
|
|
9d2e4524eb | ||
|
|
1390b4ead6 | ||
|
|
734eaba613 | ||
|
|
26c64c9763 | ||
|
|
86fb7ebcaa | ||
|
|
b2eebeea4e | ||
|
|
794b947c1e | ||
|
|
ec58b1293d | ||
|
|
695948aac9 | ||
|
|
9741c7a66b | ||
|
|
f363544542 | ||
|
|
2f0c7d7c54 | ||
|
|
dddca779c6 | ||
|
|
6fd06f8f5c | ||
|
|
a694d3dd20 | ||
|
|
1f27126215 | ||
|
|
343cefcfec | ||
|
|
fa245e66e9 | ||
|
|
1559d5b924 | ||
|
|
7be2931d09 | ||
|
|
83aa363aa0 | ||
|
|
f5f34448be | ||
|
|
14986933d6 | ||
|
|
8e35c4d922 | ||
|
|
d6608d687c | ||
|
|
436641bb07 | ||
|
|
554128d8e3 | ||
|
|
3c934d9758 | ||
|
|
1e17d90c5d | ||
|
|
fe3e788b89 | ||
|
|
2772b6c520 | ||
|
|
90ec9e2496 | ||
|
|
78eb2edba3 | ||
|
|
a88a1f79d4 | ||
|
|
1255a2560e | ||
|
|
156bca4692 | ||
|
|
e22274760c | ||
|
|
6ded9fa3f7 | ||
|
|
191191e538 | ||
|
|
8d63fadb2a | ||
|
|
2a43f18c20 | ||
|
|
20779914d4 | ||
|
|
fa4b57dfd8 | ||
|
|
90128d0b28 | ||
|
|
81a19552d7 | ||
|
|
801a2dad26 | ||
|
|
5efdde0944 | ||
|
|
2a8b5d2711 | ||
|
|
e43295e2dd | ||
|
|
0e942f09bc | ||
|
|
062504298b | ||
|
|
d30efca930 | ||
|
|
82b67d65e0 | ||
|
|
85efc505d5 | ||
|
|
af15bcb113 | ||
|
|
f4306629db | ||
|
|
173cd294e9 | ||
|
|
076d8f762f | ||
|
|
eddbd55141 | ||
|
|
2991f5339e | ||
|
|
e5b7dbfe8e | ||
|
|
1630f03209 | ||
|
|
9cdd6c669c | ||
|
|
3c87e7be9e | ||
|
|
4b704f5d99 | ||
|
|
31a0f3b642 | ||
|
|
554a135370 | ||
|
|
9f79f4d7fe | ||
|
|
64a08d92ff | ||
|
|
42b62058a3 | ||
|
|
2d35ea1eec | ||
|
|
331b95805d | ||
|
|
b4aa736586 | ||
|
|
ec2078d339 | ||
|
|
16b6d33526 | ||
|
|
96b05c9333 | ||
|
|
83f81c9dca | ||
|
|
ab5c5a9768 | ||
|
|
56ef65276c | ||
|
|
467d948ac1 | ||
|
|
82b335cded | ||
|
|
f9417645d2 | ||
|
|
c8d6e9fa9e | ||
|
|
8f7625c883 | ||
|
|
2752e5fece | ||
|
|
8a96739120 | ||
|
|
99f1e7a7a3 | ||
|
|
688b2ca946 | ||
|
|
71ba45c2f9 | ||
|
|
b2443d1ca2 | ||
|
|
ade07381c1 | ||
|
|
228c0ab57b | ||
|
|
43a081c82d | ||
|
|
c51b900777 | ||
|
|
0cfb6b0ca7 | ||
|
|
51cf1c9ff5 | ||
|
|
05ed8b14f1 | ||
|
|
55c95cba5b | ||
|
|
5fae8eeb7d | ||
|
|
8da0ed6ace | ||
|
|
ab63e3862e | ||
|
|
6683e9853f | ||
|
|
fe59952384 | ||
|
|
96fe2e8a49 | ||
|
|
badb562ebe | ||
|
|
4d95dc8d36 | ||
|
|
6b2de0c719 | ||
|
|
e1c37ba5ca | ||
|
|
ef79c90d75 | ||
|
|
319a71598a | ||
|
|
ce5057e58d | ||
|
|
e8737b4223 | ||
|
|
767f82aa3d | ||
|
|
5f85d9e716 | ||
|
|
74536d4948 | ||
|
|
5979d236bc | ||
|
|
641c8e1d60 | ||
|
|
208611543c | ||
|
|
dfad0dea3d | ||
|
|
f7cd34d9da | ||
|
|
db37b43465 | ||
|
|
617d927472 | ||
|
|
63a6d5e106 | ||
|
|
e58eead1fd | ||
|
|
52d33aef7a | ||
|
|
2aa45ebb3a | ||
|
|
eb730689b0 | ||
|
|
7369449c49 | ||
|
|
22550d9022 | ||
|
|
150209dc8f | ||
|
|
4702b6a00c | ||
|
|
122fe892af | ||
|
|
faaab0d3a1 | ||
|
|
eaccded888 | ||
|
|
273f9a5c71 | ||
|
|
3b58f30f3d | ||
|
|
a4533926a3 | ||
|
|
c4284e5f10 | ||
|
|
c2d386f8ec | ||
|
|
2c52598d1c | ||
|
|
13abd19cf5 | ||
|
|
dc3caff0b5 | ||
|
|
efa88a329d | ||
|
|
605cbf8fc3 | ||
|
|
5bc4c0a993 | ||
|
|
033ce4f6f6 | ||
|
|
bbf0324cb1 | ||
|
|
e0618a487b | ||
|
|
0b539dfafb | ||
|
|
e83bd2e67a | ||
|
|
f09c3695f4 | ||
|
|
3cd5ff5bd7 | ||
|
|
07fac1c321 | ||
|
|
39fde7b75a | ||
|
|
9032a6f70b | ||
|
|
fcb1dd53b1 | ||
|
|
9852d5d06c | ||
|
|
11bfff6736 | ||
|
|
1f9581bc65 | ||
|
|
3c8243ed21 | ||
|
|
d223507785 | ||
|
|
ed03ae279b | ||
|
|
59b9003552 | ||
|
|
c8a1437880 | ||
|
|
937fcbdb17 | ||
|
|
bde3dbfa66 | ||
|
|
3146b8a76b | ||
|
|
bec5422883 | ||
|
|
85f617a0ef | ||
|
|
c55e41a816 | ||
|
|
3c66f77454 | ||
|
|
f82f8fb738 | ||
|
|
3fb69aa921 | ||
|
|
9da13680e5 | ||
|
|
6daf1cf211 | ||
|
|
f4494f379a | ||
|
|
fef7336509 | ||
|
|
5c17b50ba5 | ||
|
|
e8f48ce5ff | ||
|
|
5fdf8f11fc | ||
|
|
cde711ab3e | ||
|
|
981414297c | ||
|
|
a6b5cd1db0 | ||
|
|
477f7cf125 | ||
|
|
9b9469622f | ||
|
|
346d0dd6d4 | ||
|
|
9ff3015e15 | ||
|
|
3ac8a64cdd | ||
|
|
9be2fa335a | ||
|
|
89d336e455 | ||
|
|
e0ae20697b | ||
|
|
7b87af9d4d | ||
|
|
5bb16bb39f | ||
|
|
e64deac9bb | ||
|
|
bbac3fc253 | ||
|
|
48105e7b45 | ||
|
|
65e32fc687 | ||
|
|
f82a81dc9a | ||
|
|
ac76b0088a | ||
|
|
f03e694a92 | ||
|
|
1e0f1d6865 | ||
|
|
3da1dda00f | ||
|
|
3a792024eb | ||
|
|
596c6f799b | ||
|
|
d3d5e5c913 | ||
|
|
86ec7b2757 | ||
|
|
7586b27de3 | ||
|
|
4bc0e5974e | ||
|
|
3be20a22ee | ||
|
|
653e5cb3ec | ||
|
|
5756718433 | ||
|
|
2df48e0d23 | ||
|
|
1a321aaad8 | ||
|
|
1072fcfe9a | ||
|
|
5ad5baa800 | ||
|
|
e6ada84bef | ||
|
|
d59df9ab1f | ||
|
|
25d3150f6f | ||
|
|
d1f1730d77 | ||
|
|
eea163dcfe | ||
|
|
1396906d96 | ||
|
|
3443536d7c | ||
|
|
11ded6c52c | ||
|
|
c79fb57a2d | ||
|
|
5902a6c22a | ||
|
|
25c183ac6e | ||
|
|
5b04b61bb0 | ||
|
|
96c61c7dfb | ||
|
|
69e714c96e | ||
|
|
cf40b2bfda | ||
|
|
6dcca28fae | ||
|
|
c0b2709580 | ||
|
|
cdb7a0cd63 | ||
|
|
0c9b0c5fbe | ||
|
|
dca9444fc6 | ||
|
|
271e4236a0 | ||
|
|
03757a4c4e | ||
|
|
cc404823cc | ||
|
|
e0eab278dd | ||
|
|
46be2e2a9c | ||
|
|
16aeafbfb6 | ||
|
|
1f368b4b37 | ||
|
|
ffb5a4f927 | ||
|
|
8098cee611 | ||
|
|
7ae5995de6 | ||
|
|
c42d91edc6 | ||
|
|
9aee9a3452 | ||
|
|
e32028964b | ||
|
|
7583b78d70 | ||
|
|
28d87c5898 | ||
|
|
7cc98709cc | ||
|
|
7c3db07789 | ||
|
|
967bdbdb98 | ||
|
|
a325ec1f88 | ||
|
|
85f9c10617 | ||
|
|
d80ca96dfa | ||
|
|
b28c918aad | ||
|
|
5cf2486f7f | ||
|
|
a563e0c78b | ||
|
|
cc7222f7ac | ||
|
|
9c47d55de9 | ||
|
|
399b9800c3 | ||
|
|
115ab27f56 | ||
|
|
5c6aacb95c | ||
|
|
bdc4d2f79b | ||
|
|
32d9d76faa | ||
|
|
07bb9f43f1 | ||
|
|
23d9ecb4dc | ||
|
|
a75420a272 | ||
|
|
9ecf431cd8 | ||
|
|
f557670077 | ||
|
|
c355318b03 | ||
|
|
29e0aeee5d | ||
|
|
a2135e93ba | ||
|
|
2203c84e12 | ||
|
|
abf69f6e97 | ||
|
|
eea88d8860 | ||
|
|
b77077b06d | ||
|
|
5df6d0470f | ||
|
|
8dbbd028da | ||
|
|
e6c4792151 | ||
|
|
4c142b8791 |
No files matched your search
@@ -9,7 +9,7 @@ on:
|
||||
# types: [published]
|
||||
push:
|
||||
branches:
|
||||
- 'docker_updates'
|
||||
- 'Mythic3.3'
|
||||
- 'master'
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
@@ -43,7 +43,7 @@ env:
|
||||
# Set the container image version to the Github release tag
|
||||
VERSION: ${{ github.ref_name }}
|
||||
|
||||
# Branch for pushing release changes (TODO: Change this to the main branch when the rewrite is finished)
|
||||
# Branch for pushing release changes
|
||||
RELEASE_BRANCH: master
|
||||
|
||||
jobs:
|
||||
@@ -72,6 +72,8 @@ jobs:
|
||||
# the following are unique to this job
|
||||
- name: Lowercase the server container image name
|
||||
run: echo "MYTHIC_SERVER_IMAGE_NAME=${MYTHIC_SERVER_IMAGE_NAME,,}" >> ${GITHUB_ENV}
|
||||
- name: Copy version file over
|
||||
run: cp VERSION mythic-docker/src/VERSION
|
||||
- name: Build and push the server container image
|
||||
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
|
||||
with:
|
||||
@@ -118,7 +120,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
|
||||
with:
|
||||
context: postgres-docker
|
||||
file: postgres-docker/.docker/Dockerfile
|
||||
file: postgres-docker/Dockerfile
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_POSTGRES_IMAGE_NAME }}:${{ env.VERSION }}
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_POSTGRES_IMAGE_NAME }}:latest
|
||||
@@ -160,7 +162,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
|
||||
with:
|
||||
context: rabbitmq-docker
|
||||
file: rabbitmq-docker/.docker/Dockerfile
|
||||
file: rabbitmq-docker/Dockerfile
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_RABBITMQ_IMAGE_NAME }}:${{ env.VERSION }}
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_RABBITMQ_IMAGE_NAME }}:latest
|
||||
@@ -202,7 +204,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
|
||||
with:
|
||||
context: documentation-docker
|
||||
file: documentation-docker/.docker/Dockerfile
|
||||
file: documentation-docker/Dockerfile
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_DOCUMENTATION_IMAGE_NAME }}:${{ env.VERSION }}
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_DOCUMENTATION_IMAGE_NAME }}:latest
|
||||
@@ -246,7 +248,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
|
||||
with:
|
||||
context: jupyter-docker
|
||||
file: jupyter-docker/.docker/Dockerfile
|
||||
file: jupyter-docker/Dockerfile
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_JUPYTER_IMAGE_NAME }}:${{ env.VERSION }}
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_JUPYTER_IMAGE_NAME }}:latest
|
||||
@@ -290,7 +292,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
|
||||
with:
|
||||
context: hasura-docker
|
||||
file: hasura-docker/.docker/Dockerfile
|
||||
file: hasura-docker/Dockerfile
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_GRAPHQL_IMAGE_NAME }}:${{ env.VERSION }}
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_GRAPHQL_IMAGE_NAME }}:latest
|
||||
@@ -334,7 +336,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
|
||||
with:
|
||||
context: nginx-docker
|
||||
file: nginx-docker/.docker/Dockerfile
|
||||
file: nginx-docker/Dockerfile
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_NGINX_IMAGE_NAME }}:${{ env.VERSION }}
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_NGINX_IMAGE_NAME }}:latest
|
||||
@@ -356,6 +358,8 @@ jobs:
|
||||
# Pull in the repository code
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4 # ref: https://github.com/marketplace/actions/checkout
|
||||
with:
|
||||
lfs: true
|
||||
- name: Log in to the container registry
|
||||
uses: docker/login-action@v3 # ref: https://github.com/marketplace/actions/docker-login
|
||||
with:
|
||||
@@ -378,7 +382,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
|
||||
with:
|
||||
context: mythic-react-docker
|
||||
file: mythic-react-docker/.docker/Dockerfile
|
||||
file: mythic-react-docker/Dockerfile
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_REACT_IMAGE_NAME }}:${{ env.VERSION }}
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_REACT_IMAGE_NAME }}:latest
|
||||
@@ -436,7 +440,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
|
||||
with:
|
||||
context: Mythic_CLI
|
||||
file: Mythic_CLI/.docker/Dockerfile
|
||||
file: Mythic_CLI/Dockerfile
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_CLI_IMAGE_NAME }}:${{ env.VERSION }}
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_CLI_IMAGE_NAME }}:latest
|
||||
@@ -548,7 +552,7 @@ jobs:
|
||||
uses: EndBug/add-and-commit@v9 # ref: https://github.com/marketplace/actions/add-commit
|
||||
with:
|
||||
# Only add the Dockerfile changes. Nothing else should have been modified
|
||||
add: "['mythic-docker/Dockerfile', 'Mythic_CLI/Dockerfile', 'Mythic_CLI/Makefile', 'Mythic_CLI/src/cmd/config/constants.go', 'postgres-docker/Dockerfile', 'rabbitmq-docker/Dockerfile', 'documentation-docker/Dockerfile', 'jupyter-docker/Dockerfile', 'hasura-docker/Dockerfile', 'nginx-docker/Dockerfile', 'mythic-react-docker/Dockerfile']"
|
||||
add: "['Mythic_CLI/Makefile', 'Mythic_CLI/src/cmd/config/constants.go']"
|
||||
# Use the Github actions bot for the commit author
|
||||
default_author: github_actions
|
||||
committer_email: github-actions[bot]@users.noreply.github.com
|
||||
|
||||
@@ -25,6 +25,7 @@ venv.bak/
|
||||
# ssl certs
|
||||
ssl/
|
||||
# Mythic files
|
||||
nginx-docker/config/blockips.conf
|
||||
mythic-docker/src/Mythic
|
||||
mythic_access.*
|
||||
mythic_sync/
|
||||
@@ -164,3 +165,8 @@ _ReSharper*/
|
||||
.vscode/
|
||||
|
||||
jupyter-docker/jupyter/MythicExamples/.ipynb_checkpoints/
|
||||
|
||||
nginx-docker/config/blockips.conf
|
||||
/MythicReactUI/build/
|
||||
/MythicReactUI/node_modules/
|
||||
/.gocache/
|
||||
+1238
File diff suppressed because it is too large.
Load diff
@@ -130,4 +130,49 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
SOFTWARE.
|
||||
|
||||
MIT license
|
||||
===========
|
||||
|
||||
Copyright (c) 2017 sql.js authors (see AUTHORS)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
|
||||
# Some portions of the Makefile taken from:
|
||||
Copyright 2017 Ryusei Yamaguchi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,26 @@
|
||||
const webpack = require('webpack');
|
||||
module.exports = function override(config, env) {
|
||||
config.resolve.fallback = {
|
||||
//url: require.resolve('url'),
|
||||
fs: false,
|
||||
assert: require.resolve('assert'),
|
||||
crypto: require.resolve('crypto-browserify'),
|
||||
path: require.resolve('path-browserify'),
|
||||
//http: require.resolve('stream-http'),
|
||||
//https: require.resolve('https-browserify'),
|
||||
//os: require.resolve('os-browserify/browser'),
|
||||
//buffer: require.resolve('buffer'),
|
||||
stream: require.resolve('stream-browserify'),
|
||||
};
|
||||
/*
|
||||
config.plugins.push(
|
||||
new webpack.ProvidePlugin({
|
||||
process: 'process/browser',
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
);
|
||||
|
||||
*/
|
||||
|
||||
return config;
|
||||
}
|
||||
Generated
+6614
-5956
File diff suppressed because it is too large.
Load diff
+48
-41
@@ -6,66 +6,73 @@
|
||||
"author": "@its_a_feature_",
|
||||
"description": "React UI for Mythic C2 Framework",
|
||||
"devDependencies": {
|
||||
"react-scripts": "^5.0.1"
|
||||
"buffer": "^6.0.3",
|
||||
"crypto-browserify": "^3.12.1",
|
||||
"react-app-rewired": "^2.2.1",
|
||||
"react-scripts": "^5.0.1",
|
||||
"stream-browserify": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.8.6",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.4.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@mui/icons-material": "^5.14.16",
|
||||
"@mui/lab": "^5.0.0-alpha.138",
|
||||
"@mui/material": "^5.14.16",
|
||||
"@mui/x-charts": "^6.18.7",
|
||||
"@mui/x-data-grid": "^6.18.6",
|
||||
"@mui/x-date-pickers": "^6.17.0",
|
||||
"ace-builds": "^1.31.1",
|
||||
"anser": "^2.1.1",
|
||||
"dayjs": "^1.11.10",
|
||||
"elkjs": "^0.8.2",
|
||||
"graphql": "^16.8.1",
|
||||
"graphql-ws": "^5.14.2",
|
||||
"@apollo/client": "^3.13.8",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@hello-pangea/dnd": "^17.0.0",
|
||||
"@mui/icons-material": "^7.0.2",
|
||||
"@mui/lab": "^7.0.0-beta.11",
|
||||
"@mui/material": "^7.0.2",
|
||||
"@mui/utils": "^7.0.2",
|
||||
"@mui/x-charts": "^8.1.0",
|
||||
"@mui/x-data-grid": "^8.1.0",
|
||||
"@mui/x-date-pickers": "^8.1.0",
|
||||
"@xyflow/react": "^12.6.0",
|
||||
"ace-builds": "^1.40.1",
|
||||
"anser": "^2.3.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"elkjs": "^0.10.0",
|
||||
"graphql": "^16.10.0",
|
||||
"graphql-ws": "^6.0.4",
|
||||
"html-to-image": "^1.11.11",
|
||||
"html2canvas": "^1.4.1",
|
||||
"ip6": "^0.2.10",
|
||||
"json5": "^2.2.3",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"material-react-table": "^2.9.2",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.43",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"path-browserify": "^1.0.1",
|
||||
"randexp": "^0.5.3",
|
||||
"react": "^18.2.0",
|
||||
"react-ace": "^10.1.0",
|
||||
"react": "^18.3.1",
|
||||
"react-ace": "^14.0.1",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-moment": "^1.1.3",
|
||||
"react-router-dom": "^6.18.0",
|
||||
"react-router-dom": "^7.13.1",
|
||||
"react-scrollbar-size": "^5.0.0",
|
||||
"react-split": "^2.0.14",
|
||||
"react-toastify": "^9.1.3",
|
||||
"react-virtualized": "^9.22.5",
|
||||
"react-virtualized-auto-sizer": "^1.0.20",
|
||||
"react-window": "^1.8.9",
|
||||
"reactflow": "^11.9.4",
|
||||
"react-toastify": "^11.0.5",
|
||||
"react-tooltip": "^5.28.1",
|
||||
"react-virtualized": "^9.22.6",
|
||||
"react-virtualized-auto-sizer": "^1.0.26",
|
||||
"react-window": "^1.8.11",
|
||||
"save-svg-as-png": "^1.4.17",
|
||||
"semver": "^7.5.4",
|
||||
"styled-components": "^6.1.0",
|
||||
"tough-cookie": "^4.1.3",
|
||||
"semver": "^7.6.2",
|
||||
"sql.js": "^1.13.0",
|
||||
"styled-components": "^6.1.17",
|
||||
"uuid": "^9.0.1",
|
||||
"webpack": "^5.89.0",
|
||||
"webpack": "^5.105.0",
|
||||
"word-wrap": "^1.2.5"
|
||||
},
|
||||
"scripts": {
|
||||
"react-start": "CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=true WDS_POLLING=true BROWSER=none PORT=3000 WDS_SOCKET_PORT=7443 WDS_SOCKET_PATH=ws react-scripts start",
|
||||
"react-build": "react-scripts build",
|
||||
"react-test": "react-scripts test",
|
||||
"react-start": "CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=true WDS_POLLING=true BROWSER=none PORT=3000 WDS_SOCKET_PORT=7443 WDS_SOCKET_PATH=ws react-app-rewired start",
|
||||
"react-build": "react-app-rewired build",
|
||||
"react-test": "react-app-rewired test",
|
||||
"react-eject": "react-scripts eject",
|
||||
"build": "npm run react-build",
|
||||
"start": "cross-env BROWSER=none CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=true WDS_POLLING=true PORT=3000 WDS_SOCKET_PORT=7443 WDS_SOCKET_PATH=ws npm run react-start"
|
||||
"start": "BROWSER=none CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=true WDS_POLLING=true PORT=3000 WDS_SOCKET_PORT=7443 WDS_SOCKET_PATH=ws npm run react-start"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.7.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Isolation_Mode" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
|
||||
y="0px" viewBox="0 0 720 720" style="enable-background:new 0 0 720 720;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#E01F2F;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M631.71,544.4c-2.42-0.54-7.34-1.83-13.64-4.16c0.33,0.12,0.65,0.26,0.97,0.39
|
||||
C624.37,542.61,628.45,543.68,631.71,544.4z"/>
|
||||
<path class="st0" d="M633.4,544.75L633.4,544.75c-0.53-0.11-1.11-0.23-1.69-0.35C632.8,544.65,633.4,544.75,633.4,544.75z"/>
|
||||
<path class="st0" d="M141.27,535.72C123.48,559.12,99,591.36,97.89,593.2c-1.18,1.97-7.99,13.54-20.39,18.46
|
||||
c-17.32,5.6-21.78-2.86-21.78-2.86s-20.56,0.4-19.7,17.05h34.95v-0.02h33.31c0,0,0.58-14.58,15.16-17.49l60.92-48.82
|
||||
C156.9,555.69,146.15,544.52,141.27,535.72z"/>
|
||||
<g>
|
||||
<path class="st0" d="M487.03,525.19l-76.29,72.09l72.21-76.16c-0.7-0.7-1.42-1.4-2.16-2.12c-9.15-8.9-17.68-13.07-26.83-13.07
|
||||
c-10.91,0-22.71,5.71-37.09,13.4c2.02,14.66,2.11,33.84-7.43,46.78c-0.07,0.09-0.14,0.19-0.21,0.28
|
||||
c-10.71,14.14-19.46,25.74-20.11,26.82c-1.19,1.98-8.11,13.75-20.72,18.61c-0.46,0.18-0.9,0.3-1.33,0.44
|
||||
c-0.51,0.18-1.04,0.33-1.58,0.49c-0.7,0.19-1.4,0.37-2.07,0.49c-0.02,0-0.02,0-0.04,0c-11.13,2.07-16.38-4.44-16.38-4.44
|
||||
s-20.56,0.4-19.7,17.05h34.95v-0.02h33.28c0,0,0.58-14.58,15.16-17.49c0,0,0,0,0.02-0.02c0.05-0.05,0.25-0.19,0.54-0.44l0.44-0.35
|
||||
c6.99-5.78,49.59-41.09,73.3-66.94c3-3.27,5.69-6.39,7.96-9.27c-0.25-0.25-0.49-0.51-0.74-0.76
|
||||
C490.59,528.89,488.89,527.1,487.03,525.19z"/>
|
||||
<path class="st0" d="M384.77,207c0,0-0.02,20.99,7.18,27.11c7.2,6.15,22.41,1.46,32.15-9.76c11.84-13.64,18.61,5.09,18.61,5.09
|
||||
c0.37-14.56-12.78-15.8-12.78-15.8c14.72-7.18,20.65,8.89,20.65,8.89c-0.67-7.2-4.21-15.58-16.17-15.56
|
||||
c-16.51,0.04-18.79,17.6-33.3,20.07C386.6,229.51,384.77,207,384.77,207z"/>
|
||||
<path class="st0" d="M681.85,338.39c-2.6-12.08-7.18-23.27-13-33.56c-5.83-10.29-13.01-19.69-21.41-28.04
|
||||
c-8.38-8.34-18.05-15.77-29.27-21.3c-11.13-5.51-23.25-8.78-35.14-10.08c-11.92-1.3-23.62-0.74-34.79,0.84
|
||||
c-11.15,1.6-21.88,4.23-31.96,7.08c-2.55,0.72-4.85,1.35-7.64,2.23c-1.35,0.42-2.67,0.79-4.04,1.26l-4.07,1.4
|
||||
c-5.44,1.98-11.05,4.44-16.19,7.24c-1.32,0.68-2.53,1.39-3.76,2.09c-1.25,0.7-2.37,1.39-3.53,2.07l-6.46,3.88
|
||||
c-4.11,2.46-8.08,4.64-12.19,6.71c-4.16,2.02-8.38,4-12.78,5.81l-3.28,1.37l-3.49,1.4l-6.92,2.74
|
||||
c-9.15,3.56-18.16,6.76-27.22,9.32c-9.04,2.58-18.12,4.51-27.2,5.64c-9.08,1.12-18.14,1.4-27.13,0.74
|
||||
c-1.12-0.09-2.25-0.14-3.37-0.23l-3.34-0.42l-3.35-0.42c-0.53-0.05-1.18-0.18-1.79-0.26l-1.81-0.28
|
||||
c-4.78-0.77-9.4-1.62-13.96-2.63c-9.11-2.02-17.91-4.69-25.94-8.2c-8.03-3.51-15.24-7.92-20.67-13.21
|
||||
c-5.46-5.3-8.94-11.31-10.55-18.25c-1.6-7.01-1.3-15.05,0.76-22.79c1-3.88,2.42-7.69,4.23-11.31c1.79-3.64,3.93-7.09,6.43-10.24
|
||||
c2.48-3.18,5.25-6.08,8.31-8.57c3.04-2.51,6.32-4.69,9.82-6.39c1.72-0.91,3.58-1.51,5.36-2.28c1.86-0.53,3.69-1.23,5.62-1.56
|
||||
c3.81-0.86,7.76-1.32,11.82-1.26c0.02,0,0.04,0,0.05,0c2.83,0.02,5.62,0.49,8.34,1.21c1.46,0.39,2.97,0.74,4.5,1.04
|
||||
c3.93,0.77,7.39,1.58,10.47,2.44c0.23,0.09,0.46,0.18,0.68,0.26l0.04-0.07c3,0.84,5.62,1.72,7.87,2.58v-0.11
|
||||
c0.86,0.37,1.69,0.76,2.46,1.12c0.02,0,0.02,0,0.04,0.02c10.78,5.22,12.05,9.69,12.05,9.69c0.04,0.09,0.09,0.12,0.12,0.21
|
||||
c2.05,2.79,3.48,5.57,4.95,7.88c5.87,8.99,8.34,11.03,8.34,11.03l1.86-3.83c-7.55-8.34-9.78-32.65-9.78-32.65
|
||||
s15.95-3.3,23.13,0.12c6.95,3.35,0.95,16.12,0.6,16.74c5.65-2.35,11.43-13.79,11.43-13.79c4.74-2.69,3.53-11.17,3.53-11.17
|
||||
c-18.95-22.83-44.53-26.03-65.87-22.14c-2.48,0.04-4.92,0.14-7.34,0.33c-4.5,0.33-9.01,0.4-13.5,0.11
|
||||
c-1.18-0.07-2.35-0.12-3.53-0.16c-6.09-0.18-12.29,0.39-18.37,1.65c-3.07,0.54-6.04,1.51-9.04,2.39
|
||||
c-2.92,1.11-5.88,2.12-8.68,3.51c-5.65,2.65-10.94,6.02-15.77,9.87c-4.85,3.85-9.2,8.24-13.03,13c-3.86,4.74-7.18,9.89-9.96,15.3
|
||||
c-2.79,5.41-5.04,11.1-6.67,17c-3.28,11.78-4.27,24.53-1.62,37.37c1.39,6.41,3.69,12.78,6.92,18.56
|
||||
c3.21,5.8,7.27,11.01,11.73,15.51c4.48,4.51,9.34,8.34,14.36,11.63c5.02,3.28,10.2,6.06,15.44,8.45
|
||||
c10.47,4.78,21.13,8.1,31.77,10.62c5.3,1.26,10.64,2.32,15.86,3.23l1.93,0.33c0.65,0.11,1.23,0.23,1.97,0.32l4.18,0.6l4.2,0.58
|
||||
c1.4,0.14,2.81,0.25,4.21,0.37c11.22,0.97,22.5,0.76,33.56-0.44c22.14-2.37,43.22-8.82,62.78-16.17l7.25-2.76l3.58-1.39l3.74-1.49
|
||||
c4.99-1.97,9.99-4.21,14.96-6.55c4.95-2.41,9.9-5.06,14.54-7.74c4.65-2.69,8.78-5.22,12.57-7.16c3.79-1.98,7.52-3.55,11.61-4.99
|
||||
l3.13-1.02c1.04-0.35,2.16-0.63,3.23-0.95c2.05-0.61,4.65-1.3,6.97-1.93c9.45-2.55,18.49-4.58,27.41-5.74
|
||||
c8.9-1.12,17.61-1.39,25.83-0.39c8.22,1,15.93,3.25,22.93,6.81c6.94,3.51,13.54,8.6,19.44,14.61
|
||||
c5.88,6.01,11.06,12.92,15.12,20.32c4.07,7.38,7.09,15.21,8.68,22.99c1.55,7.76,1.6,15.38,0.02,22.23
|
||||
c-1.6,6.85-4.71,12.98-9.52,18.32c-4.74,5.36-11.08,9.92-18.28,13.4c-7.2,3.48-15.26,5.9-23.39,7.11
|
||||
c-4.06,0.61-8.15,0.91-12.12,0.88c-1,0.02-1.97-0.05-2.95-0.09l-2.81-0.28c-1.91-0.07-3.65-0.51-5.44-0.81
|
||||
c-0.91-0.11-1.74-0.4-2.6-0.65c-0.86-0.26-1.72-0.44-2.58-0.74c-1.69-0.68-3.39-1.18-5.11-2.05l-1.3-0.58l-0.65-0.3l-0.67-0.37
|
||||
l-2.67-1.44c-0.9-0.49-1.86-1.11-2.79-1.63l-1.4-0.84l-1.49-0.95c-3.86-2.42-8.29-5.37-13.01-8.25l-3.65-2.18
|
||||
c-1.21-0.72-2.56-1.42-3.85-2.12c-2.55-1.44-5.36-2.72-8.1-4.02c-0.68-0.33-1.39-0.63-2.11-0.91l-2.14-0.86
|
||||
c-1.44-0.56-2.86-1.18-4.32-1.67l-4.43-1.42c-1.48-0.47-2.95-0.95-4.46-1.26l-4.48-1.07c-0.76-0.18-1.49-0.35-2.25-0.51
|
||||
l-2.27-0.39c-3-0.56-6.04-0.9-9.06-1.25c-6.08-0.54-12.15-0.67-18.18-0.37c-1.49,0.09-2.99,0.25-4.48,0.35l-2.25,0.19l-2.21,0.3
|
||||
l-4.43,0.6l-4.36,0.81c-11.61,2.21-22.62,5.88-32.96,10.41c-5.18,2.25-10.19,4.76-15.08,7.39c-2,1.12-4.02,2.23-6.01,3.39
|
||||
c-36.46,10.5-75.81,5.09-107.56-16.07c-0.84-0.56-1.72-1.16-2.6-1.76c-1.09-0.09-2.04-0.25-3.14-0.35
|
||||
c-3.74-0.49-7.38-0.65-11.06-0.65c-4.23,0-8.38,0.4-12.42,1.09l-2.9,0.51l-2.99,0.74c-0.95,0.19-1.84,0.49-2.84,0.76
|
||||
c-0.95,0.35-1.93,0.54-2.79,0.95c-3.85,1.3-7.48,3.09-11.12,4.99c2.9-2.95,6.18-5.55,9.87-7.57c3.74-2,7.64-3.76,11.71-4.85
|
||||
c4.11-1.14,8.39-1.84,12.57-2.09c2.9-0.16,5.69-0.16,8.62,0c-6.22-7.73-19.79-23.18-31.14-27.08c0,0-39.74-30.17-52.4-39.72
|
||||
c-12.71-9.68-27.17-27.13-24.94-32.86c0,0,9.57-13.96,17.89-16.54c8.27-2.7,10.47-12.28,10.47-12.28s-12.28,3.49-27.57,3.93
|
||||
s-22.23,4.39-22.23,4.39s-8.78-7.04-18.81-8.34c0,0,0.84-30.15,40.64-49.4c39.78-19.18,86.49-44.5,104.42-98.2
|
||||
c-1.49,2.35-24.97,39.6-53.28,50.1c-29.26,10.92-66.43,23.18-78.2,30.64c-11.85,7.43-34.56,32.35-47.7,79.13l-49.31,57.72
|
||||
c0,0,2.58,6.97,6.94,6.53c0,0,1.3,9.57,10.01,5.62c0,0,2.23,10.08,0.46,14.01c-1.76,3.95,4.43,16.16,12.66,18.35l-0.79-4.79
|
||||
c0,0,2.16,5.23,9.57,7.43c0,0-7.83-10.06-4.85-19.69c2.56-7.92,1.53-11.77,1.05-12.91l3.79,7.67c0,0,1.28-10.47-4.04-18.68
|
||||
c1.93,2.49,11.36,12.92,31.26,5.94c1.79-1.62,3.49-3.28,5.04-4.99l2.18-2.53c0.76-0.81,1.32-1.76,2-2.65
|
||||
c1.39-1.7,2.49-3.58,3.83-5.43c0.6-0.95,1.16-1.93,1.7-2.95c0.6-0.95,1.19-1.93,1.9-2.88c1.11-2,2.25-3.99,3.6-6.04
|
||||
c-0.16,2.39-0.49,4.74-0.9,7.08c-0.39,1.19-0.65,2.35-0.98,3.49l-0.65,1.69c-0.21,0.61-0.3,1.16-0.6,1.7
|
||||
c-0.49,1.09-0.98,2.23-1.55,3.28c-0.49,1.14-1.09,2.12-1.84,3.2c-0.6,0.98-1.23,2.09-1.93,3.04l-2.35,2.83
|
||||
c-1.69,1.84-3.58,3.6-5.57,5.15c-0.46,4.23-1.7,10.22-4.39,15.73c-0.39,0.91-0.79,1.76-1.14,2.55c1.19-1,24.15-19.55,60.08-3.06
|
||||
l1.9,0.79l-0.98,1.84l-2.65,4.58c4.79-1.14,9.68-1.19,14.47-0.11c6.41,1.55,11.77,6.09,15.3,11.27c3.69,5.2,5.74,11.22,7.48,17
|
||||
c0.84,2.9,1.79,5.83,3.42,8.24c1.65,2.44,4.39,4.04,7.48,4.5c-3.09-0.05-6.23-1.35-8.18-3.9c-2.05-2.49-3.2-5.43-4.34-8.24
|
||||
c-2.2-5.72-4.39-11.41-8.1-16.03c-3.58-4.6-8.38-8.24-13.91-9.54c-5.53-1.3-11.66-0.49-16.74,1.7l-4.95,2.09l2.55-4.74l3.79-6.83
|
||||
c-7.43-3.04-15.4-4.64-23.32-3.9c-8.62,0.76-16.95,3.99-24.43,8.57c-0.05,0.05-0.11,0.09-0.16,0.16
|
||||
c-9.71,5.83-17.4,14.45-22.93,24.32c-6.08,10.82-13.91,25.13-19.14,35.6c-9.68,19.19-18.35,22.74-18,31.45
|
||||
c0.46,8.73-4.34,38.88-7.78,45.03c0,0,0.79,9.68,10.87,8.71c0,0,0,7.48,10.01,6.57c0,0-6.13-9.17,2.69-13.56
|
||||
c8.78-4.34,14.4-0.39,16.56,4.79c2.2,5.27-4.79,12.85-10.22,8.82l-2.44-3.99c0,0-6.97,11.36,6.53,14.42c0,0,1.35,7.92,15.35,7.48
|
||||
c0,0-7.87-3.93-1.69-10.08c0,0,10.41-3.93,8.32-23.62c0,0,0.25-1.4,1.19-2.25c0.14-1.88,0.32-3.78,0.65-5.62
|
||||
c0.6-3.99,1.33-7.88,3.23-11.82c0.86-1.93,2.05-3.99,4.25-5.57l0.65-0.46l0.54-0.35c0.3-0.25,0.65-0.46,1.04-0.79
|
||||
c0.56-0.49,1.35-1.05,1.84-1.69c2.49-2.55,4.69-5.58,6.48-8.87c1.98-3.2,3.69-6.69,5.18-10.19c-0.3,3.9-1.28,7.64-2.6,11.38
|
||||
c-1.28,3.74-3.04,7.38-5.62,10.66c-0.65,0.84-1.35,1.63-2.11,2.39c-0.39,0.39-0.79,0.68-1.23,1.11l-0.6,0.49l-0.35,0.25
|
||||
l-0.19,0.16c-0.81,0.74-1.62,2.12-2.25,3.74c-1.25,3.14-2.05,6.78-2.55,10.41c-0.35,2.2-0.6,4.34-0.79,6.48
|
||||
c0.76,0.7,1.49,1.6,2.35,2.74c7.13,9.78,0.79,25.18,23.72,36.46c-0.84-2.09-1.69-4.28-2.42-6.48c-2.3-7.08-3.85-14.56-4.23-22.02
|
||||
c-0.26-3.79-0.21-7.57,0-11.38c0.19-3.74,0.79-7.48,1.39-11.22c0.44-1.84,0.74-3.74,1.19-5.53l0.68-2.79l0.81-2.69
|
||||
c0.54-1.79,1.19-3.6,1.88-5.37l0.79-2.55c0.37-0.79,0.56-1.6,0.81-2.49c1.93-6.73,2.83-13.77,1.9-20.63
|
||||
c-0.9-6.78-3.49-13.4-7.78-18.79c-0.98-1.35-2.14-2.65-3.42-3.79c-1.25-1.14-2.65-2.16-4.09-3.09c-1.46-0.9-2.99-1.6-4.64-2.14
|
||||
c-1.55-0.51-3.2-0.79-4.9-0.84c-3.55-0.16-6.94,0.49-10.03,2.35c-1.49,0.9-2.9,2.04-4.04,3.39c-1.25,1.3-2.35,2.84-3.06,4.48
|
||||
c0.91-3.53,3.06-6.88,6.2-9.27c2.93-2.42,6.92-3.74,10.85-3.93c1.95-0.09,3.93,0,5.9,0.46c1.93,0.46,3.88,1.11,5.62,2.09
|
||||
c1.76,0.84,3.44,2,4.93,3.2c1.55,1.23,2.99,2.6,4.28,4.04c2.65,2.95,4.83,6.29,6.34,9.87c1.69,3.58,2.9,7.38,3.34,11.26
|
||||
c1.14,7.78,0.39,15.66-1.95,23.04c-0.25,0.9-0.54,1.83-0.84,2.74l-0.98,2.65c-0.61,1.7-1.25,3.39-1.76,5.09
|
||||
c-1.16,3.39-2.09,6.88-2.9,10.38c-0.65,3.49-1.39,7.02-1.74,10.62c-0.3,3.53-0.6,7.18-0.49,10.76c0,3.6,0.19,7.18,0.54,10.76
|
||||
c0.39,3.6,1.09,7.13,1.83,10.71c0.65,2.79,1.51,5.57,2.35,8.32c0,0.05,0.05,0.05,0.05,0.05s0,0.05,0.05,0.16
|
||||
c1.33,4.13,3.09,8.11,5.13,11.98c2.79,3.88,6.94,8.08,13.26,11.56c6.34,3.42,14.82,6.27,26.18,7.32
|
||||
c41.97,3.93,41.06,13.12,46.77,20.55c0,0,3.93-15.26-4.79-22.74c0,0,6.08-0.46,21.39,14.86c15.3,15.31,25.41,7.88,25.41,7.88
|
||||
s-11.45,1.3-16.6-7.88c-5.27-9.17-7.52-30.56-14.51-34.93v-1.79c-1.09-1.25-2.2-2.55-3.35-3.9c-2.55-3.14-4.83-6.43-7.13-9.87
|
||||
c-2.12-3.39-4.23-6.92-5.83-10.85c3.49,2.39,6.53,5.2,9.48,8.01c2.35,2.41,4.53,4.85,6.73,7.38c2.2,2.6,4.28,5.23,6.23,8.04
|
||||
c3.14,4.53,5.9,9.11,8.13,14.36h0.05c13.72,17.44,32.15,43.62,28.57,55.07c-1.55,4.74-3.79,7.64-6.18,9.38
|
||||
c-6.78,4.88-15.26,0.65-15.26,0.65s-20.58,0.44-19.69,17.05h62.5c0,0,7.43-10.52,16.26-13.96c8.62-3.51-19.37-72.54-19.74-73.48
|
||||
c0.67,0.39,26.39,15.45,73.86-6.53c48.12-22.28,72.19-48.89,104.89-17.03c32.75,31.91,21.83,34.09,121.54,53.79
|
||||
c0,0,34.1,13.12,19.65,38.88c0,0,0.11,0.44,0,1.09v-1.09c-10.22,9.15-16.4,1.3-16.4,1.3s-20.55,0.44-19.63,17.05h67.59
|
||||
c0,0-1.84-10.52,7.32-13.96c9.24-3.53-10.06-27.97-16.16-50.29c-4.58-16.89-4.53-14.75-15.35-16.86c0,0-0.02,0-0.04-0.02
|
||||
c-0.02,0-0.04-0.02-0.07-0.04c-0.05,0-0.16,0-0.25-0.05h-0.25c-0.25-0.05-0.6-0.16-1.09-0.25h-0.05c-0.21-0.05-0.4-0.09-0.61-0.16
|
||||
c-0.11-0.02-0.19-0.05-0.3-0.07s-0.23-0.05-0.35-0.07c-0.05,0-0.11,0-0.19-0.05c-0.11,0-0.21-0.05-0.25-0.05
|
||||
c-0.05-0.04-0.09-0.04-0.16-0.05c-0.02,0-0.04-0.02-0.05-0.02s-0.04-0.02-0.04-0.02c-0.04-0.02-0.04-0.02-0.07-0.04
|
||||
c-0.05,0-0.09-0.05-0.19-0.05h-0.11c-0.09-0.05-0.19-0.05-0.3-0.05c-0.7-0.21-1.55-0.4-2.41-0.65c-0.09,0-0.16-0.05-0.25-0.05
|
||||
c-0.11,0-0.16-0.05-0.25-0.09c-0.26-0.09-0.53-0.18-0.81-0.26c-0.19-0.05-0.39-0.11-0.56-0.18c-0.18-0.07-0.37-0.11-0.54-0.18
|
||||
c-0.18-0.05-0.37-0.11-0.54-0.18c-0.3-0.11-0.6-0.19-0.91-0.26c-1.14-0.4-2.46-0.79-3.74-1.3c-0.16-0.11-0.33-0.16-0.49-0.25
|
||||
c0-0.02-0.02-0.02-0.02-0.02h-0.02c-0.11-0.04-0.18-0.05-0.23-0.09c-0.04-0.02-0.07-0.04-0.11-0.05c0,0,0,0-0.02-0.02
|
||||
s-0.05-0.04-0.09-0.05c-8.9-3.34-20.58-8.85-31.89-17.65c0,0-0.04-0.04-0.11-0.09c-3.9-3.04-7.76-6.44-11.43-10.29
|
||||
c-0.04-0.04-0.07-0.07-0.11-0.11c-13.26-12.8-34.72-36.3-47.31-64.59c-0.35-0.79-0.74-1.58-1.12-2.35c0-0.02-0.02-0.02-0.02-0.02
|
||||
c-3.25-6.37-7.92-12.15-13.49-17.37c-0.3-0.28-0.6-0.56-0.9-0.83c0.02,0,0.04,0.02,0.05,0.02c-4.79-4.69-10.1-8.9-15.61-12.66
|
||||
l2.81,0.84c0.93,0.3,1.84,0.7,2.77,1.05l1.39,0.53c0.46,0.16,0.93,0.35,1.39,0.58c1.83,0.88,3.67,1.63,5.53,2.69
|
||||
c0.93,0.51,1.84,0.95,2.81,1.51l2.88,1.69c0.95,0.53,2,1.21,2.99,1.83l3.09,1.91l3.21,2.02l3.44,2.12
|
||||
c9.15,5.71,21.35,11.94,34.28,14.31l4.81,0.83c1.62,0.32,3.18,0.35,4.78,0.51l4.76,0.37c1.56,0.04,3.13,0.11,4.65,0.07
|
||||
c6.16-0.02,12.2-0.56,18.16-1.53c11.87-1.93,23.43-5.51,34.4-10.98c5.5-2.72,10.82-5.97,15.89-9.75
|
||||
c5.08-3.76,9.85-8.15,14.14-13.1c4.3-4.93,8.08-10.5,11.08-16.47c3-5.99,5.25-12.35,6.64-18.83
|
||||
C684.92,363.52,684.41,350.44,681.85,338.39z M379.22,175.86l15,10.55l-23.46-8.52L379.22,175.86z M85.99,279.4l11.61-13.42h8.43
|
||||
L85.99,279.4z M150.4,270.47l5.6-12.15l29.31-10.52l-23.99,11.36L150.4,270.47z M70.99,449.54l11.61-13.36h8.52L70.99,449.54z
|
||||
M170.54,512c-3.69-5.58-5.64-12.36-6.04-19.11c-0.25-3.34-0.05-6.73,0.35-10.06c0.25-0.86,0.35-1.7,0.54-2.6
|
||||
c0.19-1.05,0.4-1.6,0.56-2.44c0.44-1.49,0.68-2.99,0.98-4.53c1.14-6.18,1.69-12.52,1.79-18.81c0.32-12.61-1.39-25.32-4.64-37.48
|
||||
c-0.84-3.09-1.84-6.04-2.99-8.92c-1.14-2.95-2.6-5.74-4.23-8.32c-1.7-2.65-3.69-5.08-6.13-7.02c-1.25-0.95-2.53-1.81-3.99-2.49
|
||||
c-1.44-0.65-2.95-1.16-4.53-1.25c3.2-0.46,6.59,0.65,9.48,2.35c1.74,0.98,3.39,2.23,4.79,3.6c0.65-1.46,1.19-2.9,1.44-4.41
|
||||
c0.35-1.53,0.44-3.09,0.44-4.64c-0.09-3.04-0.79-6.04-1.93-8.87c-1.09-2.84-2.6-5.58-4.34-8.18c-1.4-2.18-2.95-4.34-4.65-6.27
|
||||
l-2.48,0.81c-1.25,0.46-2.55,0.95-3.83,1.39c-1.26,0.54-2.46,1.09-3.69,1.65l-1.7,0.98c-0.6,0.3-1.19,0.6-1.79,1
|
||||
c-2.25,1.39-4.28,3.04-5.94,5.09c-0.51,0.54-0.84,1.09-1.16,1.63c-0.25,0.3-0.46,0.6-0.6,0.9c-0.09,0.3-0.35,0.6-0.44,0.91
|
||||
c-0.54,1.23-0.9,2.53-1.05,3.99c-0.3-1.4-0.3-2.9-0.11-4.36c0.11-0.68,0.35-1.44,0.54-2.12c0.35-0.65,0.6-1.46,0.95-2.05
|
||||
c0.74-1.39,1.55-2.53,2.6-3.74l1.44-1.63c0.61-0.49,1.16-0.95,1.76-1.46c0.54-0.54,1.19-0.93,1.74-1.33l1.84-1.3
|
||||
c1.25-0.65,2.55-1.46,3.83-2.05c1.39-0.6,2.69-1.25,4.04-1.76l4.09-1.44l1.35-0.46l1,1.04c1.28,1.35,2.25,2.65,3.28,3.99
|
||||
c1.05,1.35,2,2.74,2.79,4.2c1.79,2.88,3.2,6.02,4.13,9.41c0.4,1.6,0.79,3.28,0.9,5.04c0.16,1.74,0.05,3.49-0.16,5.18
|
||||
c-0.35,1.69-0.79,3.34-1.49,4.88c-0.81,1.46-1.7,2.84-2.76,4c0.98,0.95,1.84,1.9,2.76,2.97c2.12,2.6,3.88,5.44,5.43,8.43
|
||||
c1.55,2.95,2.84,6.09,3.88,9.17c2.2,6.29,3.69,12.71,4.76,19.25c0.95,6.53,1.33,13.15,1.14,19.79
|
||||
c-0.16,6.62-0.98,13.29-2.46,19.79c-0.35,1.6-0.9,3.2-1.39,4.83c-0.19,0.76-0.54,1.69-0.68,2.2c-0.19,0.68-0.35,1.49-0.46,2.2
|
||||
c-0.65,2.99-1,6.11-1,9.22c0,3.09,0.35,6.18,1.05,9.18c0.65,3.09,1.63,5.99,3.09,8.82c1.44,2.74,3.28,5.34,5.43,7.67
|
||||
c2.11,2.34,4.53,4.5,7.18,6.32C179.46,522.03,174.23,517.67,170.54,512z M207.58,458.21c0.25,2.49,0.76,4.97,1.35,7.57
|
||||
c0.65,2.48,1.6,4.93,2.53,7.48c-2.04-1.79-3.79-4.04-5.18-6.43c-1.35-2.44-2.41-5.02-3.2-7.81c-0.68-2.69-1.04-5.5-1.23-8.29
|
||||
c-0.16-2.83,0-5.62,0.35-8.36c0.6-5.6,2.2-10.98,4.34-16c2.2-4.99,4.83-9.78,8.53-13.91c-1.49,5.18-3.04,10.26-4.25,15.4
|
||||
c-1.23,5.04-2.18,10.12-2.9,15.21C207.32,448.1,207.18,453.17,207.58,458.21z M601.46,548.79c5.23,0.49,10.36,1.46,15.44,2.9
|
||||
c2.49,0.76,4.99,1.65,7.34,2.79c2.39,1.19,4.69,2.6,6.73,4.43l1.46,1.55c0.39,0.49,0.74,0.98,1.19,1.49
|
||||
c0.79,1.05,1.65,2.11,2.46,3.2c1.49,2.09,3.04,4.23,4.37,6.5c2.65,4.48,5.08,9.11,7.18,14l-0.19,0.11
|
||||
c-3.2-4.2-6.09-8.53-8.97-12.82l-4.36-6.48c-0.65-1.09-1.39-2.14-2.12-3.25c-0.4-0.49-0.81-1.04-1.11-1.6l-1.14-1.35
|
||||
c-1.84-1.74-3.9-3.23-6.13-4.37c-2.3-1.25-4.64-2.23-7.13-3.09c-4.83-1.74-9.87-2.93-15.07-3.74L601.46,548.79z M438.98,462.85
|
||||
c-1.6,0.95-3.04,2.09-4.39,3.28c-1.35,1.14-2.6,2.55-3.69,3.99c-0.6,0.65-1.05,1.39-1.55,2.16c-0.46,0.76-0.9,1.55-1.19,2.23v0.05
|
||||
l-0.16,0.21c-2.55,4.07-5.34,8.08-8.59,11.68c-1.39,1.93-3.18,3.64-4.88,5.43c-0.84,0.84-1.69,1.6-2.6,2.49
|
||||
c-0.95,0.79-1.74,1.63-2.74,2.39c-7.39,6.34-15.72,11.52-24.53,15.51c-8.78,4.04-18.16,6.78-27.57,8.52
|
||||
c-4.79,0.84-9.48,1.46-14.29,1.65c-2.46,0.16-4.85,0.19-7.24,0.16c-2.35-0.05-4.69-0.16-7.04-0.46
|
||||
c9.41-1.05,18.65-2.84,27.61-5.34c4.55-1.19,8.92-2.69,13.36-4.28c4.28-1.6,8.62-3.34,12.77-5.39
|
||||
c8.32-3.99,16.24-8.82,23.37-14.59c7.24-5.72,13.66-12.42,19.44-19.74c1.05-1.79,2.25-3.2,3.64-4.55
|
||||
c1.35-1.44,2.95-2.69,4.58-3.74c1.65-1.09,3.34-1.98,5.15-2.63c1.79-0.76,3.74-1.16,5.57-1.51c-0.84,0.4-1.79,0.7-2.58,1.19
|
||||
C440.67,461.95,439.77,462.35,438.98,462.85z M507.47,503.47l2.49,3.6c0.84,1.11,1.6,2.28,2.49,3.39l2.65,3.34l1.39,1.65
|
||||
l1.39,1.49c1.02,1.05,2,2.05,2.99,3.09l3.09,2.84c1.09,1.05,2.2,1.9,3.28,2.79c1.09,0.9,2.2,1.79,3.39,2.63
|
||||
c4.53,3.39,9.43,6.39,14.51,8.94c5.18,2.55,10.57,4.74,16.16,6.18l-0.16,0.54c-5.8-0.98-11.38-2.79-16.7-5.09
|
||||
c-5.34-2.23-10.57-4.97-15.44-8.22c-1.32-0.68-2.49-1.63-3.71-2.49c-1.19-0.9-2.34-1.69-3.48-2.67l-3.35-2.84
|
||||
c-1.09-0.93-2.2-1.98-3.28-2.99l-1.55-1.53l-1.49-1.65l-3.04-3.28c-0.95-1.14-1.81-2.28-2.69-3.49l-2.69-3.42l-1.3-1.65
|
||||
l-1.39-1.55c-0.86-1.09-1.84-1.98-2.79-2.99c-2.05-1.83-4.18-3.64-6.43-5.18c-2.3-1.62-4.58-2.95-7.08-4.23
|
||||
c-2.39-1.25-4.88-2.46-7.52-3.39c-10.27-4.04-21.58-5.9-33.12-6.2l-0.09-0.6c5.69-1.55,11.63-2.05,17.54-1.81
|
||||
c2.93,0.11,5.99,0.46,8.87,0.91c2.99,0.49,5.94,1.3,8.82,2.2c1.4,0.46,2.79,0.98,4.2,1.55c1.39,0.6,2.79,1.23,4.14,1.98
|
||||
c1.33,0.65,2.58,1.46,3.93,2.3c0.6,0.39,1.25,0.84,1.9,1.23c0.61,0.51,1.21,0.95,1.81,1.4C500.04,494,504.13,498.55,507.47,503.47
|
||||
z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 305 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 184 KiB |
+196
-8
@@ -1,42 +1,230 @@
|
||||
import { makeVar } from '@apollo/client';
|
||||
import React from 'react';
|
||||
import {restartWebsockets} from "./index";
|
||||
import {snackActions} from "./components/utilities/Snackbar";
|
||||
|
||||
export const meState = makeVar({loggedIn:false, user: null, access_token: null, refresh_token: null});
|
||||
export const menuOpen = makeVar(false);
|
||||
export const alertCount = makeVar(0);
|
||||
export const taskTimestampDisplayFieldOptions = [
|
||||
{
|
||||
name: "timestamp",
|
||||
display: "Latest Timestamp for anything task related"
|
||||
},
|
||||
{
|
||||
name: "status_timestamp_preprocessing",
|
||||
display: "When Operator Submitted Task"
|
||||
},
|
||||
{
|
||||
name: "status_timestamp_processing",
|
||||
display: "When Agent Picked up Task",
|
||||
}
|
||||
]
|
||||
export const taskingContextFieldsOptions = ["impersonation_context", "cwd", "user", "host", "ip", "pid", "process_short_name", "extra_info", "architecture"].sort();
|
||||
export const defaultShortcuts = [
|
||||
"ActiveCallbacks", "Payloads", "PayloadTypesAndC2",
|
||||
"Operations", "SearchFiles", "SearchProxies",
|
||||
"CreatePayload", "Eventing",
|
||||
].sort();
|
||||
export const operatorSettingDefaults = {
|
||||
fontSize: 12,
|
||||
navBarOpen: false,
|
||||
fontFamily: "Verdana, Arial, sans-serif",
|
||||
showMedia: true,
|
||||
hideUsernames: false,
|
||||
showIP: false,
|
||||
showHostname: false,
|
||||
showOPSECBypassUsername: false,
|
||||
showCallbackGroups: false,
|
||||
useDisplayParamsForCLIHistory: true,
|
||||
interactType: "interactSplit",
|
||||
taskTimestampDisplayField: "timestamp",
|
||||
callbacks_table_columns: ["Interact", "Host", "Domain", "User", "Description", "Last Checkin", "Agent", "IP", "PID"],
|
||||
callbacks_table_filters: {},
|
||||
autoTaskLsOnEmptyDirectories: false,
|
||||
hideBrowserTasking: false,
|
||||
hideTaskingContext: false,
|
||||
taskingContextFields: ["impersonation_context", "cwd"],
|
||||
["experiment-responseStreamLimit"]: 200,
|
||||
sideShortcuts: defaultShortcuts,
|
||||
palette: {
|
||||
primary: {
|
||||
dark: "#75859b",
|
||||
light: "#75859b",
|
||||
},
|
||||
error: {
|
||||
dark: '#bd5142',
|
||||
light: '#c42c32'
|
||||
},
|
||||
success: {
|
||||
dark: '#85b089',
|
||||
light: '#0e7004',
|
||||
},
|
||||
secondary: {
|
||||
dark: '#bebebe',
|
||||
light: '#a6a5a5'
|
||||
},
|
||||
info: {
|
||||
dark: '#84b4dc',
|
||||
light: '#4990b2'
|
||||
},
|
||||
warning: {
|
||||
dark: "#dc8455",
|
||||
light: "#ffb74d",
|
||||
},
|
||||
background: {
|
||||
dark: '#282828',
|
||||
light: '#f6f6f6'
|
||||
},
|
||||
paper: {
|
||||
dark: '#282828',
|
||||
light: '#ececec'
|
||||
},
|
||||
tableHeader: {
|
||||
dark: '#484848',
|
||||
light: '#c4c4c4'
|
||||
},
|
||||
tableHover: {
|
||||
dark: "#3c3c3c",
|
||||
light: "#e8e8e8",
|
||||
},
|
||||
pageHeader: {
|
||||
dark: '#1b2025',
|
||||
light: '#706c6e'
|
||||
},
|
||||
text: {
|
||||
dark: "#e4e4e4",
|
||||
light: "#000000",
|
||||
},
|
||||
selectedCallbackColor: {
|
||||
dark: '#26456e',
|
||||
light: '#c6e5f6',
|
||||
},
|
||||
selectedCallbackHierarchyColor: {
|
||||
dark: '#273e5d',
|
||||
light: '#deeff8',
|
||||
},
|
||||
backgroundImage: {
|
||||
dark: null,
|
||||
light: null
|
||||
},
|
||||
navBarIcons: {
|
||||
dark: '#ffffff',
|
||||
light: '#ffffff'
|
||||
},
|
||||
navBarText: {
|
||||
dark: '#ffffff',
|
||||
light: '#ffffff'
|
||||
},
|
||||
navBarColor: {
|
||||
dark: "#194573",
|
||||
light: "#3b606d",
|
||||
},
|
||||
navBarBottomColor: {
|
||||
dark: "#330814",
|
||||
light: "#283581",
|
||||
},
|
||||
taskPromptTextColor: {
|
||||
dark: '#bebebe',
|
||||
light: '#a6a5a5'
|
||||
},
|
||||
taskPromptCommandTextColor: {
|
||||
dark: "#e4e4e4",
|
||||
light: "#000000",
|
||||
},
|
||||
taskContextColor: {
|
||||
dark: "#122848",
|
||||
light: "#acc0da",
|
||||
},
|
||||
taskContextImpersonationColor: {
|
||||
dark: "#641616",
|
||||
light: "#dec0c0",
|
||||
},
|
||||
taskContextExtraColor: {
|
||||
dark: "#2a5953",
|
||||
light: "#a7ce9d",
|
||||
},
|
||||
emptyFolderColor: {
|
||||
dark: '#bebebe',
|
||||
light: '#a6a5a5'
|
||||
},
|
||||
outputBackgroundColor: {
|
||||
dark: '#282828',
|
||||
light: '#f6f6f6'
|
||||
},
|
||||
outputTextColor: {
|
||||
dark: '#f6f6f6',
|
||||
light: '#282828',
|
||||
},
|
||||
borderColor: {
|
||||
dark: "#595858",
|
||||
light: "#c5c5c5"
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const mePreferences = makeVar(operatorSettingDefaults);
|
||||
|
||||
|
||||
export const successfulLogin = (data) => {
|
||||
localStorage.setItem("access_token", data.access_token);
|
||||
localStorage.setItem("refresh_token", data.refresh_token);
|
||||
localStorage.setItem("user", JSON.stringify(data.user));
|
||||
let now = new Date();
|
||||
let serverNow = new Date(data.user.current_utc_time);
|
||||
const difference = (serverNow.getTime() - now.getTime());
|
||||
let me = {...data.user};
|
||||
me.server_skew = difference;
|
||||
me.login_time = now;
|
||||
meState({
|
||||
loggedIn: true,
|
||||
...data
|
||||
access_token: data.access_token,
|
||||
refresh_token: data.refresh_token,
|
||||
user: {
|
||||
...me
|
||||
}
|
||||
});
|
||||
localStorage.setItem("user", JSON.stringify(me));
|
||||
restartWebsockets();
|
||||
}
|
||||
export const successfulRefresh = (data) => {
|
||||
localStorage.setItem("access_token", data.access_token);
|
||||
localStorage.setItem("refresh_token", data.refresh_token);
|
||||
let now = new Date();
|
||||
let serverNow = new Date(data.user.current_utc_time);
|
||||
const difference = (serverNow.getTime() - now.getTime()) ;
|
||||
let me = {...meState().user};
|
||||
me.server_skew = difference;
|
||||
me.login_time = now;
|
||||
meState({
|
||||
loggedIn: true,
|
||||
access_token: localStorage.getItem("access_token"),
|
||||
...meState()
|
||||
access_token: data.access_token,
|
||||
refresh_token: data.refresh_token,
|
||||
user: {
|
||||
...me
|
||||
}
|
||||
});
|
||||
localStorage.setItem("user", JSON.stringify(me));
|
||||
}
|
||||
export const FailedRefresh = () =>{
|
||||
export const FailedRefresh = (restart_websockets) =>{
|
||||
console.log("failed refresh");
|
||||
localStorage.removeItem("access_token");
|
||||
localStorage.removeItem("refresh_token");
|
||||
localStorage.removeItem("user");
|
||||
// retrieve all cookies
|
||||
let Cookies = document.cookie.split(';');
|
||||
// set past expiry to all cookies
|
||||
for (let i = 0; i < Cookies.length; i++) {
|
||||
document.cookie = Cookies[i] + "=; expires="+ new Date(0).toUTCString();
|
||||
}
|
||||
meState({
|
||||
loggedIn: false,
|
||||
access_token: null,
|
||||
refresh_token: null,
|
||||
user: null
|
||||
});
|
||||
mePreferences(operatorSettingDefaults);
|
||||
snackActions.clearAll();
|
||||
restartWebsockets();
|
||||
if(restart_websockets){
|
||||
restartWebsockets();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+380
-118
@@ -12,19 +12,20 @@ import { Home } from './pages/Home/Home';
|
||||
import { LoggedInRoute } from './utilities/LoggedInRoute';
|
||||
import { Callbacks } from './pages/Callbacks/Callbacks';
|
||||
import { Search } from './pages/Search/Search';
|
||||
import { ConsumingServices } from './pages/ConsumingServices/ConsumingServices';
|
||||
import React, {createContext} from 'react';
|
||||
import { TopAppBar } from './TopAppBar';
|
||||
import { Typography } from '@mui/material';
|
||||
import { useReactiveVar } from '@apollo/client';
|
||||
import { useDarkMode } from './utilities/useDarkMode';
|
||||
import { SingleTaskView } from './pages/SingleTaskView/SingleTaskView';
|
||||
import { createTheme, ThemeProvider, StyledEngineProvider, adaptV4Theme } from '@mui/material/styles';
|
||||
import { createTheme, ThemeProvider, StyledEngineProvider } from '@mui/material/styles';
|
||||
import { GlobalStyles } from '../themes/GlobalStyles';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import { FailedRefresh, meState } from '../cache';
|
||||
import {FailedRefresh, mePreferences, meState, operatorSettingDefaults} from '../cache';
|
||||
import { Reporting } from './pages/Reporting/Reporting';
|
||||
import { MitreAttack } from './pages/MITRE_ATTACK/MitreAttack';
|
||||
import {Tags} from './pages/Tags/Tags';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import {useLazyQuery, gql } from '@apollo/client';
|
||||
//background-color: #282c34;
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import { useInterval } from './utilities/Time';
|
||||
@@ -33,101 +34,262 @@ import { RefreshTokenDialog } from './RefreshTokenDialog';
|
||||
import { MythicDialog } from './MythicComponents/MythicDialog';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import {Eventing} from "./pages/Eventing/Eventing";
|
||||
import {InviteForm} from "./pages/Login/InviteForm";
|
||||
import {snackActions} from "./utilities/Snackbar";
|
||||
import {TopAppBarVertical} from "./TopAppBarVertical";
|
||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||
import * as Icons from '@fortawesome/free-solid-svg-icons';
|
||||
import {Jupyter} from "./pages/Jupyter/Jupyter";
|
||||
import {Hasura} from "./pages/Hasura/Hasura";
|
||||
|
||||
// add all fas icons
|
||||
const iconList = Object
|
||||
.keys(Icons)
|
||||
.filter(key => key !== "fas" && key !== "prefix" )
|
||||
.map(icon => Icons[icon])
|
||||
|
||||
library.add(...iconList)
|
||||
|
||||
export const MeContext = createContext({});
|
||||
export const userSettingsQuery = gql`
|
||||
query getUserSettings {
|
||||
getOperatorPreferences {
|
||||
status
|
||||
error
|
||||
preferences
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
export function App(props) {
|
||||
const me = useReactiveVar(meState);
|
||||
const preferences = useReactiveVar(mePreferences);
|
||||
const [loadingPreference, setLoadingPreferences] = React.useState(true);
|
||||
const [themeMode, themeToggler] = useDarkMode();
|
||||
const localStorageFontSize = localStorage.getItem(`${me?.user?.user_id || 0}-fontSize`);
|
||||
const initialLocalStorageFontSizeValue = localStorageFontSize === null ? 12 : parseInt(localStorageFontSize);
|
||||
const localStorageFontFamily = localStorage.getItem(`${me?.user?.user_id || 0}-fontFamily`);
|
||||
const initialLocalStorageFontFamilyValue = localStorageFontFamily === null ? [
|
||||
'-apple-system',
|
||||
'BlinkMacSystemFont',
|
||||
'"Segoe UI"',
|
||||
'Roboto',
|
||||
'"Helvetica Neue"',
|
||||
'Arial',
|
||||
'sans-serif',
|
||||
'"Apple Color Emoji"',
|
||||
'"Segoe UI Emoji"',
|
||||
'"Segoe UI Symbol"',
|
||||
].join(',') : localStorageFontFamily;
|
||||
const localStorageTopColor = localStorage.getItem(`${me?.user?.user_id || 0}-topColor`);
|
||||
const initialLocalStorageTopColorValue = localStorageTopColor === null ? "#3c4d67" : localStorageTopColor;
|
||||
const theme = React.useMemo(
|
||||
() =>
|
||||
createTheme(adaptV4Theme({
|
||||
palette: {
|
||||
primary: {
|
||||
main: themeMode === "dark" ? "#465a79" : "rgb(102,121,145)",
|
||||
() => {
|
||||
try{
|
||||
return createTheme({
|
||||
transitions: {
|
||||
// So we have `transition: none;` everywhere
|
||||
create: () => 'none',
|
||||
},
|
||||
error: {
|
||||
main: themeMode === "dark" ? '#da3237' : '#c42c32',
|
||||
palette: {
|
||||
contrastThreshold: 4.5,
|
||||
//tonalOffset: 0.5,
|
||||
primary: {
|
||||
main: themeMode === "dark" ? preferences?.palette?.primary?.dark || operatorSettingDefaults.palette.primary.dark :
|
||||
preferences?.palette?.primary?.light || operatorSettingDefaults.palette.primary.light,
|
||||
},
|
||||
error: {
|
||||
main: themeMode === "dark" ? preferences?.palette?.error?.dark || operatorSettingDefaults.palette.error.dark :
|
||||
preferences?.palette?.error?.light || operatorSettingDefaults.palette.error.light,
|
||||
},
|
||||
success: {
|
||||
main: themeMode === 'dark' ? preferences?.palette?.success?.dark || operatorSettingDefaults.palette.success.dark :
|
||||
preferences?.palette?.success?.light || operatorSettingDefaults.palette.success.light,
|
||||
},
|
||||
secondary: {
|
||||
main: themeMode === 'dark' ? preferences?.palette?.secondary?.dark || operatorSettingDefaults.palette.secondary.dark :
|
||||
preferences?.palette?.secondary?.light || operatorSettingDefaults.palette.secondary.light,
|
||||
},
|
||||
info: {
|
||||
main: themeMode === 'dark' ? preferences?.palette?.info?.dark || operatorSettingDefaults.palette.info.dark :
|
||||
preferences?.palette?.info?.light || operatorSettingDefaults.palette.info.light,
|
||||
},
|
||||
warning: {
|
||||
main: themeMode === 'dark' ? preferences?.palette?.warning?.dark || operatorSettingDefaults.palette.warning.dark :
|
||||
preferences?.palette?.warning?.light || operatorSettingDefaults.palette.warning.light,
|
||||
},
|
||||
mode: themeMode,
|
||||
background: {
|
||||
contrast: themeMode === 'dark' ? preferences?.palette?.background?.light || operatorSettingDefaults.palette.background.light :
|
||||
preferences?.palette?.background?.dark || operatorSettingDefaults.palette.background.dark,
|
||||
default: themeMode === "dark" ? preferences?.palette?.background?.dark || operatorSettingDefaults.palette.background.dark :
|
||||
preferences?.palette?.background?.light || operatorSettingDefaults.palette.background.light,
|
||||
paper: themeMode === "dark" ? preferences?.palette?.paper?.dark || operatorSettingDefaults.palette.paper.dark :
|
||||
preferences?.palette?.paper?.light || operatorSettingDefaults.palette.paper.light,
|
||||
image: themeMode === "dark" ? preferences?.palette?.backgroundImage?.dark || operatorSettingDefaults.palette.backgroundImage.dark :
|
||||
preferences?.palette?.backgroundImage?.light || operatorSettingDefaults.palette.backgroundImage.light,
|
||||
},
|
||||
text: {
|
||||
primary: themeMode === 'dark' ? preferences?.palette?.text?.dark || operatorSettingDefaults.palette.text.dark :
|
||||
preferences?.palette?.text?.light || operatorSettingDefaults.palette.text.light,
|
||||
contrast: themeMode === 'dark' ? '#000' : '#fff',
|
||||
},
|
||||
graphGroupRGBA: themeMode === 'dark' ? 'rgba(57, 76, 93, 0.5)' : 'rgba(211, 215, 232, 0.5)',
|
||||
speedDialAction: themeMode === 'dark' ? '#495054' : '#ffffff',
|
||||
},
|
||||
success: {
|
||||
main: themeMode === 'dark' ? '#44b636' : '#0e7004',
|
||||
folderColor: '#f1d592',
|
||||
tableHeader: themeMode === 'dark' ? preferences?.palette?.tableHeader?.dark || operatorSettingDefaults.palette.tableHeader.dark :
|
||||
preferences?.palette?.tableHeader?.light || operatorSettingDefaults.palette.tableHeader.light,
|
||||
selectedCallbackColor: themeMode === 'dark' ? preferences?.palette?.selectedCallbackColor?.dark || operatorSettingDefaults.palette.selectedCallbackColor.dark :
|
||||
preferences?.palette?.selectedCallbackColor?.light || operatorSettingDefaults.palette.selectedCallbackColor.light,
|
||||
selectedCallbackHierarchyColor: themeMode === 'dark' ? preferences?.palette?.selectedCallbackHierarchyColor?.dark || operatorSettingDefaults.palette.selectedCallbackHierarchyColor.dark :
|
||||
preferences?.palette?.selectedCallbackHierarchyColor?.light || operatorSettingDefaults.palette.selectedCallbackHierarchyColor.light,
|
||||
tableHover: themeMode === 'dark' ? preferences?.palette?.tableHover?.dark || operatorSettingDefaults.palette.tableHover.dark :
|
||||
preferences?.palette?.tableHover?.light || operatorSettingDefaults.palette.tableHover.light,
|
||||
navBarTextIconColor: themeMode === 'dark' ? preferences?.palette?.navBarIcons?.dark || operatorSettingDefaults.palette.navBarIcons.dark :
|
||||
preferences?.palette?.navBarIcons?.light || operatorSettingDefaults.palette.navBarIcons.light,
|
||||
navBarTextColor: themeMode === 'dark' ? preferences?.palette?.navBarText?.dark || operatorSettingDefaults.palette.navBarText.dark :
|
||||
preferences?.palette?.navBarText?.light || operatorSettingDefaults.palette.navBarText.light,
|
||||
pageHeader: {
|
||||
main: themeMode === 'dark' ? preferences?.palette?.pageHeader?.dark || operatorSettingDefaults.palette.pageHeader.dark :
|
||||
preferences?.palette?.pageHeader?.light || operatorSettingDefaults.palette.pageHeader.light,
|
||||
},
|
||||
secondary: {
|
||||
main: themeMode === 'dark' ? '#bebebe' : '#a6a5a5',
|
||||
pageHeaderText: {
|
||||
main: 'white',
|
||||
},
|
||||
info: {
|
||||
main: themeMode === 'dark' ? '#2574b4' : '#4990b2',
|
||||
topAppBarColor: themeMode === 'dark' ? (preferences?.palette?.navBarColor?.dark || operatorSettingDefaults.palette.navBarColor.dark) :
|
||||
(preferences?.palette?.navBarColor?.light || operatorSettingDefaults.palette.navBarColor.light),
|
||||
topAppBarBottomColor: themeMode === 'dark' ? preferences?.palette?.navBarBottomColor?.dark || operatorSettingDefaults.palette.navBarBottomColor.dark :
|
||||
preferences?.palette?.navBarBottomColor?.light || operatorSettingDefaults.palette.navBarBottomColor.light,
|
||||
typography: {
|
||||
fontSize: 12, //preferences?.fontSize,
|
||||
fontFamily: preferences?.fontFamily
|
||||
},
|
||||
mode: themeMode,
|
||||
background: {
|
||||
contrast: themeMode === 'dark' ? '#ffffff' : '#30455e',
|
||||
default: themeMode === "dark" ? 'rgb(31, 31, 31)' : '#ffffff',
|
||||
taskPromptTextColor: themeMode === 'dark' ? preferences?.palette?.taskPromptTextColor?.dark || operatorSettingDefaults.palette.taskPromptTextColor.dark :
|
||||
preferences?.palette?.taskPromptTextColor?.light || operatorSettingDefaults.palette.taskPromptTextColor.light,
|
||||
taskPromptCommandTextColor: themeMode === 'dark' ? preferences?.palette?.taskPromptCommandTextColor?.dark || operatorSettingDefaults.palette.taskPromptCommandTextColor.dark :
|
||||
preferences?.palette?.taskPromptCommandTextColor?.light || operatorSettingDefaults.palette.taskPromptCommandTextColor.light,
|
||||
taskContextColor: themeMode === 'dark' ? preferences?.palette?.taskContextColor?.dark || operatorSettingDefaults.palette.taskContextColor.dark :
|
||||
preferences?.palette?.taskContextColor?.light || operatorSettingDefaults.palette.taskContextColor.light,
|
||||
taskContextImpersonationColor: themeMode === 'dark' ? preferences?.palette?.taskContextImpersonationColor?.dark || operatorSettingDefaults.palette.taskContextImpersonationColor.dark :
|
||||
preferences?.palette?.taskContextImpersonationColor?.light || operatorSettingDefaults.palette.taskContextImpersonationColor.light,
|
||||
taskContextExtraColor: themeMode === 'dark' ? preferences?.palette?.taskContextExtraColor?.dark || operatorSettingDefaults.palette.taskContextExtraColor.dark :
|
||||
preferences?.palette?.taskContextExtraColor?.light || operatorSettingDefaults.palette.taskContextExtraColor.light,
|
||||
emptyFolderColor: themeMode === 'dark' ? preferences?.palette?.emptyFolderColor?.dark || operatorSettingDefaults.palette.emptyFolderColor.dark :
|
||||
preferences?.palette?.emptyFolderColor?.light || operatorSettingDefaults.palette.emptyFolderColor.light,
|
||||
outputBackgroundColor: themeMode === 'dark' ? preferences?.palette?.outputBackgroundColor?.dark || operatorSettingDefaults.palette.outputBackgroundColor.dark :
|
||||
preferences?.palette?.outputBackgroundColor?.light || operatorSettingDefaults.palette.outputBackgroundColor.light,
|
||||
outputTextColor: themeMode === 'dark' ? preferences?.palette?.outputTextColor?.dark || operatorSettingDefaults.palette.outputTextColor.dark :
|
||||
preferences?.palette?.outputTextColor?.light || operatorSettingDefaults.palette.outputTextColor.light,
|
||||
borderColor: themeMode === 'dark' ? preferences?.palette?.borderColor?.dark || operatorSettingDefaults.palette.borderColor.dark :
|
||||
preferences?.palette?.borderColor?.light || operatorSettingDefaults.palette.borderColor.light,
|
||||
})
|
||||
}catch(error){
|
||||
console.log(error);
|
||||
snackActions.error(error.message);
|
||||
return createTheme({
|
||||
transitions: {
|
||||
// So we have `transition: none;` everywhere
|
||||
create: () => 'none',
|
||||
},
|
||||
listSubHeader: {
|
||||
default: themeMode === "dark" ? 'rgb(50, 50, 50)' : 'rgb(240, 240, 240)',
|
||||
palette: {
|
||||
contrastThreshold: 4.5,
|
||||
//tonalOffset: 0.5,
|
||||
primary: {
|
||||
main: themeMode === "dark" ? operatorSettingDefaults.palette.primary.dark :
|
||||
operatorSettingDefaults.palette.primary.light,
|
||||
},
|
||||
error: {
|
||||
main: themeMode === "dark" ? operatorSettingDefaults.palette.error.dark :
|
||||
operatorSettingDefaults.palette.error.light,
|
||||
},
|
||||
success: {
|
||||
main: themeMode === 'dark' ? operatorSettingDefaults.palette.success.dark :
|
||||
operatorSettingDefaults.palette.success.light,
|
||||
},
|
||||
secondary: {
|
||||
main: themeMode === 'dark' ? operatorSettingDefaults.palette.secondary.dark :
|
||||
operatorSettingDefaults.palette.secondary.light,
|
||||
},
|
||||
info: {
|
||||
main: themeMode === 'dark' ? operatorSettingDefaults.palette.info.dark :
|
||||
operatorSettingDefaults.palette.info.light,
|
||||
},
|
||||
warning: {
|
||||
main: themeMode === 'dark' ? operatorSettingDefaults.palette.warning.dark :
|
||||
operatorSettingDefaults.palette.warning.light,
|
||||
},
|
||||
mode: themeMode,
|
||||
background: {
|
||||
contrast: themeMode === 'dark' ?operatorSettingDefaults.palette.background.light :
|
||||
operatorSettingDefaults.palette.background.dark,
|
||||
default: themeMode === "dark" ? operatorSettingDefaults.palette.background.dark :
|
||||
operatorSettingDefaults.palette.background.light,
|
||||
paper: themeMode === "dark" ? operatorSettingDefaults.palette.paper.dark :
|
||||
operatorSettingDefaults.palette.paper.light,
|
||||
image: themeMode === "dark" ? operatorSettingDefaults.palette.backgroundImage.dark :
|
||||
operatorSettingDefaults.palette.backgroundImage.light,
|
||||
},
|
||||
text: {
|
||||
primary: themeMode === 'dark' ? operatorSettingDefaults.palette.text.dark :
|
||||
operatorSettingDefaults.palette.text.light,
|
||||
contrast: themeMode === 'dark' ? '#000' : '#fff',
|
||||
},
|
||||
graphGroupRGBA: themeMode === 'dark' ? 'rgba(57, 76, 93, 0.5)' : 'rgba(211, 215, 232, 0.5)',
|
||||
speedDialAction: themeMode === 'dark' ? '#495054' : '#ffffff',
|
||||
},
|
||||
text: {
|
||||
contrast: themeMode === 'dark' ? '#000' : '#fff',
|
||||
folderColor: '#f1d592',
|
||||
tableHeader: themeMode === 'dark' ? operatorSettingDefaults.palette.tableHeader.dark :
|
||||
operatorSettingDefaults.palette.tableHeader.light,
|
||||
selectedCallbackColor: themeMode === 'dark' ? operatorSettingDefaults.palette.selectedCallbackColor.dark :
|
||||
operatorSettingDefaults.palette.selectedCallbackColor.light,
|
||||
selectedCallbackHierarchyColor: themeMode === 'dark' ? operatorSettingDefaults.palette.selectedCallbackHierarchyColor.dark :
|
||||
operatorSettingDefaults.palette.selectedCallbackHierarchyColor.light,
|
||||
tableHover: themeMode === 'dark' ? operatorSettingDefaults.palette.tableHover.dark :
|
||||
operatorSettingDefaults.palette.tableHover.light,
|
||||
navBarTextIconColor: themeMode === 'dark' ? operatorSettingDefaults.palette.navBarIcons.dark :
|
||||
operatorSettingDefaults.palette.navBarIcons.light,
|
||||
navBarTextColor: themeMode === 'dark' ? operatorSettingDefaults.palette.navBarText.dark :
|
||||
operatorSettingDefaults.palette.navBarText.light,
|
||||
pageHeader: {
|
||||
main: themeMode === 'dark' ? operatorSettingDefaults.palette.pageHeader.dark :
|
||||
operatorSettingDefaults.palette.pageHeader.light,
|
||||
},
|
||||
textBackgroundColor: themeMode === 'dark' ? '#272c2f' : '#e9eaea',
|
||||
textBackgroundColorMythic: themeMode === 'dark' ? '#436b9f' : '#aadcf5',
|
||||
textBackgroundColorSuccess: themeMode === 'dark' ? '#09a21a' : '#70e373',
|
||||
textBackgroundColorError: themeMode === 'dark' ? '#9f1616' : '#f19da3',
|
||||
graphGroup: themeMode === 'dark' ? '#394c5d' : '#d3d7e8',
|
||||
graphGroupRGBA: themeMode === 'dark' ? 'rgba(57, 76, 93, 0.5)' : 'rgba(211, 215, 232, 0.5)',
|
||||
speedDialAction: themeMode === 'dark' ? '#495054' : '#ffffff',
|
||||
},
|
||||
pageHeaderColor: "white",
|
||||
folderColor: '#f1d592',
|
||||
tableHeader: '#484848',
|
||||
successOnMain: '#1ae302',
|
||||
errorOnMain: '#ff656b',
|
||||
infoOnMain: '#67ceff',
|
||||
materialReactTableHeader: themeMode === 'dark' ? '#484848' : '#d5d5d5',
|
||||
tableBorder: themeMode === 'dark' ? 'rgba(81,81,81,1)' : 'rgba(224,224,224,1)',
|
||||
tableHover: themeMode === 'dark' ? 'rgba(85,88,93)' : 'rgba(245, 245, 245)',
|
||||
pageHeader: {
|
||||
main: '#827E80',
|
||||
},
|
||||
pageHeaderSecondary: {
|
||||
main: '#444343',
|
||||
},
|
||||
pageHeaderText: {
|
||||
main: 'white',
|
||||
},
|
||||
topAppBarColor: initialLocalStorageTopColorValue,
|
||||
typography: {
|
||||
fontSize: initialLocalStorageFontSizeValue,
|
||||
fontFamily: initialLocalStorageFontFamilyValue
|
||||
},
|
||||
})),
|
||||
[themeMode]
|
||||
pageHeaderText: {
|
||||
main: 'white',
|
||||
},
|
||||
topAppBarColor: themeMode === 'dark' ? operatorSettingDefaults.palette.navBarColor.dark :
|
||||
operatorSettingDefaults.palette.navBarColor.light,
|
||||
topAppBarBottomColor: themeMode === 'dark' ? operatorSettingDefaults.palette.navBarBottomColor.dark :
|
||||
operatorSettingDefaults.palette.navBarBottomColor.light,
|
||||
emptyFolderColor: themeMode === 'dark' ? operatorSettingDefaults.palette.emptyFolderColor.dark :
|
||||
operatorSettingDefaults.palette.emptyFolderColor.light,
|
||||
outputBackgroundColor: themeMode === 'dark' ? operatorSettingDefaults.palette.outputBackgroundColor.dark :
|
||||
operatorSettingDefaults.palette.outputBackgroundColor.light,
|
||||
outputTextColor: themeMode === 'dark' ? operatorSettingDefaults.palette.outputTextColor.dark :
|
||||
operatorSettingDefaults.palette.outputTextColor.light,
|
||||
borderColor: themeMode === 'dark' ? operatorSettingDefaults.palette.borderColor.dark :
|
||||
operatorSettingDefaults.palette.borderColor.light,
|
||||
typography: {
|
||||
fontSize: 12,//operatorSettingDefaults?.fontSize,
|
||||
fontFamily: operatorSettingDefaults?.fontFamily
|
||||
},
|
||||
})
|
||||
}
|
||||
},[themeMode, loadingPreference, preferences.fontSize, preferences.fontFamily, preferences.palette]
|
||||
);
|
||||
const mountedRef = React.useRef(true);
|
||||
const [openRefreshDialog, setOpenRefreshDialog] = React.useState(false);
|
||||
const [getUserPreferences] = useLazyQuery(userSettingsQuery, {
|
||||
fetchPolicy: "no-cache",
|
||||
onCompleted: (data) => {
|
||||
//console.log("got preferences", data.getOperatorPreferences.preferences)
|
||||
if(data.getOperatorPreferences.status === "success"){
|
||||
if(data.getOperatorPreferences.preferences !== null){
|
||||
mePreferences({...preferences, ...data.getOperatorPreferences.preferences});
|
||||
}
|
||||
} else {
|
||||
snackActions.error(`Failed to get user preferences:\n${data.getOperatorPreferences.error}`);
|
||||
}
|
||||
setLoadingPreferences(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log(error);
|
||||
snackActions.error(error.message);
|
||||
setLoadingPreferences(false);
|
||||
}
|
||||
})
|
||||
useInterval( () => {
|
||||
// interval should run every 10 minutes (600000 milliseconds) to check JWT status
|
||||
let millisecondsLeft = JWTTimeLeft();
|
||||
// if we have 30min left of our token, prompt the user to extend. 30 min is 1,800,000 milliseconds
|
||||
//console.log("jwt time left: ", millisecondsLeft)
|
||||
if(millisecondsLeft <= 1800000 && !openRefreshDialog){
|
||||
if(millisecondsLeft <= 1800000 && !openRefreshDialog && me.loggedIn){
|
||||
if(isJWTValid()){
|
||||
setOpenRefreshDialog(true);
|
||||
}else{
|
||||
@@ -135,57 +297,157 @@ export function App(props) {
|
||||
}
|
||||
}
|
||||
}, 600000, mountedRef, mountedRef);
|
||||
React.useEffect( () => {
|
||||
if(me.loggedIn){
|
||||
setLoadingPreferences(true);
|
||||
getUserPreferences();
|
||||
} else {
|
||||
setLoadingPreferences(false);
|
||||
}
|
||||
}, [me.loggedIn])
|
||||
if(loadingPreference){
|
||||
// make sure we've loaded preferences before loading actual app content
|
||||
return (
|
||||
<StyledEngineProvider injectFirst>
|
||||
<ThemeProvider theme={theme}>
|
||||
<GlobalStyles theme={theme} />
|
||||
<CssBaseline />
|
||||
</ThemeProvider>
|
||||
</StyledEngineProvider>
|
||||
)
|
||||
}
|
||||
const background = theme.palette.background.image !== null ? {
|
||||
backgroundImage: "linear-gradient(" + theme.palette.background.default + "99" + "," + theme.palette.background.default + "99" + ")," + theme.palette.background.image ,
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundSize: "100% 100%"
|
||||
} : {
|
||||
backgroundColor: theme.palette.background.default
|
||||
};
|
||||
return (
|
||||
<StyledEngineProvider injectFirst>
|
||||
<ThemeProvider theme={theme}>
|
||||
<GlobalStyles theme={theme} />
|
||||
<CssBaseline />
|
||||
<ToastContainer limit={2} autoClose={3000}
|
||||
theme={themeMode}
|
||||
style={{maxWidth: "100%", minWidth: "40%", width: "40%", marginTop: "20px", display: "flex", flexWrap: "wrap",
|
||||
wordBreak: "break-all", flexDirection: "column", justifyContent: "center"}}
|
||||
pauseOnFocusLoss={false} />
|
||||
<div style={{ maxHeight: '100%', height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ minHeight: '56px', flexGrow: 0 }}>
|
||||
{me.loggedIn && me.user !== undefined && me.user !== null ? (
|
||||
<TopAppBar me={me} theme={themeMode} toggleTheme={themeToggler} />
|
||||
) : null}
|
||||
</div>
|
||||
{openRefreshDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openRefreshDialog}
|
||||
onClose={()=>{setOpenRefreshDialog(false);}}
|
||||
innerDialog={<RefreshTokenDialog
|
||||
onClose={()=>{setOpenRefreshDialog(false);}} />}
|
||||
<MeContext.Provider value={me}>
|
||||
<Tooltip id={"my-tooltip"} style={{zIndex: 100000, wordBreak: "break-word", maxWidth: "80%", whiteSpace: "pre-wrap"}}/>
|
||||
<ToastContainer limit={2} autoClose={3000}
|
||||
theme={themeMode}
|
||||
hideProgressBar={true}
|
||||
newestOnTop={true}
|
||||
stacked={false}
|
||||
style={{maxWidth: "100%", minWidth: "40%", display: "flex", flexWrap: "wrap",
|
||||
wordBreak: "break-all", flexDirection: "column", justifyContent: "center",}}
|
||||
pauseOnFocusLoss={false}
|
||||
/>
|
||||
<div style={{ maxHeight: '100%', height: '100%', display: 'flex', flexDirection: 'row', maxWidth: "100%", width:"100%",
|
||||
...background}}>
|
||||
{openRefreshDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openRefreshDialog}
|
||||
onClose={()=>{setOpenRefreshDialog(false);}}
|
||||
innerDialog={<RefreshTokenDialog
|
||||
onClose={()=>{setOpenRefreshDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
<div style={{ margin: '0px 5px 5px 5px', flexGrow: 1, flexDirection: 'column', height: "calc(100% - 4rem)", }}>
|
||||
<Routes>
|
||||
<Route path='/new/login' element={<LoginForm me={me}/>}/>
|
||||
<Route path='/' element={<LoggedInRoute me={me}><Home me={me}/></LoggedInRoute>} />
|
||||
<Route exact path='/new' element={<LoggedInRoute me={me}><Home me={me}/></LoggedInRoute>} />
|
||||
<Route exact path='/new/settings' element={<LoggedInRoute me={me}><Settings me={me}/></LoggedInRoute>} />
|
||||
<Route exact path='/new/payloadtypes' element={<LoggedInRoute me={me}><PayloadTypesC2Profiles me={me}/></LoggedInRoute>} />
|
||||
<Route exact path='/new/eventfeed' element={<LoggedInRoute me={me}><EventFeed me={me}/></LoggedInRoute>} />
|
||||
<Route exact path='/new/createpayload' element={<LoggedInRoute me={me}><CreatePayload me={me}/></LoggedInRoute>} />
|
||||
<Route exact path='/new/createwrapper' element={<LoggedInRoute me={me}><CreatePayloadWrapper me={me}/></LoggedInRoute>} />
|
||||
<Route exact path='/new/payloads' element={<LoggedInRoute me={me}><Payloads me={me} /></LoggedInRoute>} />
|
||||
<Route exact path='/new/c2profiles' element={<LoggedInRoute me={me}><PayloadTypesC2Profiles me={me}/></LoggedInRoute>} />
|
||||
<Route exact path='/new/services/' element={<LoggedInRoute me={me}><PayloadTypesC2Profiles me={me}/></LoggedInRoute>} />
|
||||
<Route exact path='/new/callbacks' element={<LoggedInRoute me={me}><Callbacks me={me}/></LoggedInRoute>} />
|
||||
<Route path='/new/search' element={<LoggedInRoute me={me}><Search history={props.history} me={me} /></LoggedInRoute>} />
|
||||
<Route exact path='/new/browserscripts' element={<LoggedInRoute me={me}><BrowserScripts me={me}/></LoggedInRoute>} />
|
||||
<Route exact path='/new/task/:taskId' element={<LoggedInRoute me={me}><SingleTaskView me={me}/></LoggedInRoute>} />
|
||||
<Route exact path='/new/tasks/by_range' element={<LoggedInRoute me={me}><SingleTaskView me={me}/></LoggedInRoute>} />
|
||||
<Route exact path='/new/operations' element={<LoggedInRoute me={me}><Operations me={me}/></LoggedInRoute>} />
|
||||
<Route exact path='/new/callbacks/:callbackDisplayId' element={<LoggedInRoute me={me}><ExpandedCallback me={me}/></LoggedInRoute>} />
|
||||
<Route exact path='/new/reporting' element={<LoggedInRoute me={me}><Reporting me={me}/></LoggedInRoute>} />
|
||||
<Route exact path='/new/mitre' element={<LoggedInRoute me={me}><MitreAttack me={me}/></LoggedInRoute>} />
|
||||
<Route exact path='/new/tagtypes' element={<LoggedInRoute me={me}><Tags me={me}/></LoggedInRoute>} />
|
||||
<Route exact path='/new/consuming_services' element={<LoggedInRoute me={me}><ConsumingServices me={me}/></LoggedInRoute>} />
|
||||
</Routes>
|
||||
{me.loggedIn && me.user !== undefined && me.user !== null &&
|
||||
<TopAppBarVertical me={me} toggleTheme={themeToggler} />
|
||||
}
|
||||
<div style={{
|
||||
maxHeight: '100%',
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<div style={{height: "5px", width: "100%", background: me.loggedIn ? `linear-gradient(25deg, ${theme.topAppBarColor}, ${theme.topAppBarBottomColor})` : ""}}/>
|
||||
{me.loggedIn && me?.user?.current_operation_banner_text !== "" &&
|
||||
<Typography style={{
|
||||
backgroundColor: me?.user?.current_operation_banner_color,
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
fontWeight: "600",
|
||||
color: "white",
|
||||
border: `1px solid ${theme.topAppBarColor || "grey"}`
|
||||
}}>
|
||||
{me?.user?.current_operation_banner_text}
|
||||
</Typography>
|
||||
}
|
||||
{me.loggedIn && me?.badConnection
|
||||
&&
|
||||
<Typography style={{
|
||||
backgroundColor: theme.palette.error.main,
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
fontWeight: "600",
|
||||
color: "white",
|
||||
border: `1px solid ${theme.topAppBarColor || "grey"}`
|
||||
}}>
|
||||
{"Can't connect to Mythic. Please check connection and refresh"}
|
||||
</Typography>
|
||||
}
|
||||
<div style={{
|
||||
margin: '0px 0px 0px 0px',
|
||||
flexGrow: 1,
|
||||
display: "flex",
|
||||
flexDirection: 'column',
|
||||
overflow:"hidden"
|
||||
}}>
|
||||
<Routes>
|
||||
<Route path='/new/login' element={<LoginForm me={me}/>}/>
|
||||
<Route path='/new/invite' element={<InviteForm me={me}/>}/>
|
||||
<Route path='/' element={<LoggedInRoute me={me}><Home me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new'
|
||||
element={<LoggedInRoute me={me}><Home me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/settings'
|
||||
element={<LoggedInRoute me={me}><Settings me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/payloadtypes'
|
||||
element={<LoggedInRoute me={me}><PayloadTypesC2Profiles
|
||||
me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/eventfeed'
|
||||
element={<LoggedInRoute me={me}><EventFeed me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/createpayload'
|
||||
element={<LoggedInRoute me={me}><CreatePayload me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/createwrapper'
|
||||
element={<LoggedInRoute me={me}><CreatePayloadWrapper
|
||||
me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/payloads'
|
||||
element={<LoggedInRoute me={me}><Payloads me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/c2profiles'
|
||||
element={<LoggedInRoute me={me}><PayloadTypesC2Profiles
|
||||
me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/services/'
|
||||
element={<LoggedInRoute me={me}><PayloadTypesC2Profiles
|
||||
me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/callbacks'
|
||||
element={<LoggedInRoute me={me}><Callbacks me={me}/></LoggedInRoute>}/>
|
||||
<Route path='/new/search'
|
||||
element={<LoggedInRoute me={me}><Search history={props.history}
|
||||
me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/browserscripts'
|
||||
element={<LoggedInRoute me={me}><BrowserScripts me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/task/:taskId'
|
||||
element={<LoggedInRoute me={me}><SingleTaskView me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/tasks/by_range'
|
||||
element={<LoggedInRoute me={me}><SingleTaskView me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/operations'
|
||||
element={<LoggedInRoute me={me}><Operations me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/callbacks/:callbackDisplayId'
|
||||
element={<LoggedInRoute me={me}><ExpandedCallback
|
||||
me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/reporting'
|
||||
element={<LoggedInRoute me={me}><Reporting me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/mitre'
|
||||
element={<LoggedInRoute me={me}><MitreAttack me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/tagtypes'
|
||||
element={<LoggedInRoute me={me}><Tags me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/eventing'
|
||||
element={<LoggedInRoute me={me}><Eventing me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/jupyter' element={<LoggedInRoute me={me}><Jupyter/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/hasura' element={<LoggedInRoute me={me}><Hasura/></LoggedInRoute>}/>
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</MeContext.Provider>
|
||||
</ThemeProvider>
|
||||
</StyledEngineProvider>
|
||||
);
|
||||
|
||||
@@ -1,26 +1,12 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import {gql, useSubscription} from '@apollo/client';
|
||||
import {snackActions} from './utilities/Snackbar';
|
||||
import {getSkewedNow} from "./utilities/Time";
|
||||
|
||||
//fromNow must be in ISO format for hasura/postgres stuff
|
||||
//new Date().toISOString() will do it
|
||||
/*
|
||||
|
||||
const subscribe_payloads = gql`
|
||||
subscription EventFeedNotificationSubscription($fromNow: timestamp!, $operation_id: Int!) {
|
||||
operationeventlog(limit: 1, where: {deleted: {_eq: false}, timestamp: {_gte: $fromNow}, operation_id: {_eq: $operation_id}}, order_by: {id: desc}) {
|
||||
operator {
|
||||
username
|
||||
}
|
||||
id
|
||||
message
|
||||
level
|
||||
resolved
|
||||
source
|
||||
}
|
||||
}
|
||||
`;
|
||||
*/
|
||||
const subscribe_payloads = gql`
|
||||
subscription EventFeedNotificationSubscription($fromNow: timestamp!) {
|
||||
operationeventlog_stream(cursor: {initial_value: {timestamp: $fromNow}, ordering: ASC}, batch_size: 1, where: {deleted: {_eq: false}}) {
|
||||
operator {
|
||||
@@ -31,54 +17,53 @@ subscription EventFeedNotificationSubscription($fromNow: timestamp!, $operation_
|
||||
level
|
||||
resolved
|
||||
source
|
||||
warning
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function EventFeedNotifications(props) {
|
||||
const me = props.me;
|
||||
const fromNow = React.useRef( (new Date()).toISOString() );
|
||||
const { loading, error, data } = useSubscription(subscribe_payloads, {
|
||||
variables: {fromNow: fromNow.current},
|
||||
const [fromNow, setFromNow] = React.useState(getSkewedNow().toISOString());
|
||||
//const fromNow = React.useRef( );
|
||||
useSubscription(subscribe_payloads, {
|
||||
variables: {fromNow: fromNow },
|
||||
fetchPolicy: "no-cache",
|
||||
shouldResubscribe: true,
|
||||
onError: (errorData) => {
|
||||
snackActions.warning("Failed to get event notifications");
|
||||
console.log(errorData);
|
||||
},
|
||||
onData: ({data}) => {
|
||||
if (data.data.operationeventlog_stream.length > 0){
|
||||
if(data.data.operationeventlog_stream[0].level === "api" ||
|
||||
data.data.operationeventlog_stream[0].level === "debug" ||
|
||||
data.data.operationeventlog_stream[0].level === "agent"
|
||||
){
|
||||
return;
|
||||
}
|
||||
if(data.data.operationeventlog_stream[0].resolved){
|
||||
return;
|
||||
}
|
||||
if(data.data.operationeventlog_stream[0].operator){
|
||||
const message = data.data.operationeventlog_stream[0].operator.username + ":\n" + data.data.operationeventlog_stream[0].message;
|
||||
if (data.data.operationeventlog_stream[0].warning) {
|
||||
snackActions.warning(message, { autoClose: 2000});
|
||||
} else {
|
||||
snackActions.info(message, { autoClose: 2000});
|
||||
}
|
||||
|
||||
}else {
|
||||
if (data.data.operationeventlog_stream[0].warning) {
|
||||
snackActions.warning(data.data.operationeventlog_stream[0].message, {autoClose: 3000});
|
||||
} else {
|
||||
snackActions.info(
|
||||
<div style={{width: "100%"}}>{data.data.operationeventlog_stream[0].message}</div>,
|
||||
{autoClose: 3000});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useEffect( () => {
|
||||
//console.log(data, loading, error, fromNow.current);
|
||||
if(!loading && !error && data && data.operationeventlog_stream.length > 0){
|
||||
if(data.operationeventlog_stream[0].source === "debug" || data.operationeventlog_stream[0].level === "debug" ){
|
||||
return;
|
||||
}
|
||||
if(data.operationeventlog_stream[0].resolved){
|
||||
return;
|
||||
}
|
||||
if(data.operationeventlog_stream[0].operator){
|
||||
const message = data.operationeventlog_stream[0].operator.username + ":\n" + data.operationeventlog_stream[0].message;
|
||||
if (data.operationeventlog_stream[0].level === "warning") {
|
||||
snackActions.warning(message, { autoClose: 2000});
|
||||
} else {
|
||||
snackActions.info(message, { autoClose: 2000});
|
||||
}
|
||||
|
||||
}else {
|
||||
if (data.operationeventlog_stream[0].level === "warning") {
|
||||
snackActions.warning(data.operationeventlog_stream[0].message, {autoClose: 3000});
|
||||
} else {
|
||||
snackActions.info(
|
||||
<div style={{width: "100%"}}>{data.operationeventlog_stream[0].message}</div>,
|
||||
{autoClose: 3000});
|
||||
}
|
||||
|
||||
}
|
||||
}else if(error){
|
||||
console.error(error);
|
||||
snackActions.error("Mythic encountered an error getting operational event stream", {autoHideDuration: 2000});
|
||||
}
|
||||
}, [loading, data, error, me.user]);
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import {MythicStyledTooltip} from "./MythicStyledTooltip";
|
||||
import WifiIcon from '@mui/icons-material/Wifi';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import { faLink } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export const MythicAgentSVGIcon = ({payload_type, style, is_p2p}) => {
|
||||
const theme = useTheme();
|
||||
if(payload_type === ""){
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<MythicStyledTooltip title={payload_type} >
|
||||
<img src={"/static/" + payload_type + "_" + theme.palette.mode + ".svg"}
|
||||
style={{...style}} />
|
||||
{is_p2p === false &&
|
||||
<WifiIcon style={{width: "20px", height: "20px", marginLeft: "-20px", marginBottom: "5px",
|
||||
borderRadius: "10px", border: "1px solid",
|
||||
backgroundColor: theme.palette.primary.main}}/>
|
||||
}
|
||||
{is_p2p === true &&
|
||||
<FontAwesomeIcon icon={faLink} style={{width: "20px", height: "20px", marginLeft: "-20px", marginBottom: "5px",
|
||||
borderRadius: "10px", border: "1px solid",
|
||||
backgroundColor: theme.palette.primary.main}} />
|
||||
}
|
||||
</MythicStyledTooltip>
|
||||
|
||||
)
|
||||
}
|
||||
export const MythicAgentSVGIconNoTooltip = ({payload_type, is_p2p, className, style}) => {
|
||||
const theme = useTheme();
|
||||
if(payload_type === ""){
|
||||
return;
|
||||
}
|
||||
const extraStyles = style ? style : {};
|
||||
return (
|
||||
<div style={{display: "inline-block", left: "50%"}} className={className}>
|
||||
<img src={"/static/" + payload_type + "_" + theme.palette.mode + ".svg"} style={{height: "20px", margin: 'auto', ...extraStyles}}/>
|
||||
{is_p2p === false &&
|
||||
<WifiIcon style={{width: "10px", height: "10px", marginLeft: "-5px", marginBottom: "0px",
|
||||
borderRadius: "10px", border: "1px solid",
|
||||
backgroundColor: theme.palette.primary.main, color: "white" }} />
|
||||
}
|
||||
{is_p2p === true &&
|
||||
<FontAwesomeIcon icon={faLink} style={{width: "10px", height: "10px", marginLeft: "-5px", marginBottom: "0px",
|
||||
borderRadius: "10px", border: "1px solid",
|
||||
backgroundColor: theme.palette.primary.main, color: "white"}} />
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
)
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import Button from '@mui/material/Button';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import MythicTextField from './MythicTextField';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
@@ -13,11 +12,35 @@ import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import { toLocalTime } from '../utilities/Time';
|
||||
|
||||
import AceEditor from 'react-ace';
|
||||
import "ace-builds/src-noconflict/mode-json";
|
||||
import "ace-builds/src-noconflict/theme-github";
|
||||
import "ace-builds/src-noconflict/theme-monokai";
|
||||
import "ace-builds/src-noconflict/ext-searchbox";
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import WrapTextIcon from '@mui/icons-material/WrapText';
|
||||
import {IconButton} from '@mui/material';
|
||||
import {MythicStyledTooltip} from "./MythicStyledTooltip";
|
||||
import Draggable from 'react-draggable';
|
||||
import {MythicDraggableDialogTitle} from "./MythicDraggableDialogTitle";
|
||||
|
||||
export function MythicDialog(props) {
|
||||
const descriptionElementRef = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
const [draggedState, setDraggedState] = React.useState({
|
||||
style: {},
|
||||
paperStyle: {
|
||||
margin: "0",
|
||||
height: "fit-content",
|
||||
width: "stretch"
|
||||
},
|
||||
containerStyle: {
|
||||
|
||||
},
|
||||
hideBackdrop: false,
|
||||
modified: false
|
||||
});
|
||||
const nodeRef = React.useRef(null);
|
||||
const descriptionElementRef = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
if (props.open) {
|
||||
const { current: descriptionElement } = descriptionElementRef;
|
||||
if (descriptionElement !== null) {
|
||||
@@ -26,54 +49,160 @@ export function MythicDialog(props) {
|
||||
}
|
||||
}, [props.open]);
|
||||
const dialogOnClick = (e) => {
|
||||
e.stopPropagation();
|
||||
//e.preventDefault();
|
||||
//e.stopPropagation();
|
||||
if(e.target.classList.length > 0 && e.target.classList.contains("MuiDialog-container")){
|
||||
props.onClose();
|
||||
props.onClose();
|
||||
}
|
||||
}
|
||||
const dialogOnContextMenu = (e) => {
|
||||
e.stopPropagation();
|
||||
}
|
||||
const handleOnClose = (event, reason) => {
|
||||
if(reason === "backdropClick" && draggedState.hideBackdrop){
|
||||
return;
|
||||
}
|
||||
props.onClose();
|
||||
}
|
||||
const onStart = (e) => {
|
||||
if(e){
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
if(!draggedState.modified){
|
||||
setDraggedState({
|
||||
style: {
|
||||
height: e.target.offsetParent.offsetHeight + "px",
|
||||
width: e.target.offsetParent.offsetWidth + "px",
|
||||
margin: "auto",
|
||||
overflowY: "auto",
|
||||
},
|
||||
paperStyle: {
|
||||
//height: e.target.offsetParent.offsetHeight + "px",
|
||||
width: e.target.offsetParent.offsetWidth + "px",
|
||||
margin: 0,
|
||||
overflowY: "auto",
|
||||
},
|
||||
containerStyle: {
|
||||
height: "fit-content",
|
||||
overflowY: "auto"
|
||||
},
|
||||
hideBackdrop: true,
|
||||
modified: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
const onStop = (e) => {
|
||||
if(e){
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Dialog
|
||||
open={props.open}
|
||||
onClose={props.onClose}
|
||||
scroll="paper"
|
||||
maxWidth={props.maxWidth}
|
||||
fullWidth={props.fullWidth}
|
||||
style={props.style}
|
||||
aria-labelledby="scroll-dialog-title"
|
||||
aria-describedby="scroll-dialog-description"
|
||||
onClick={dialogOnClick}
|
||||
<Draggable
|
||||
nodeRef={nodeRef}
|
||||
handle="#mythic-draggable-title"
|
||||
cancel={'[class*="MuiDialogContent-root"]'}
|
||||
onStart={onStart}
|
||||
onStop={onStop}
|
||||
>
|
||||
{props.innerDialog}
|
||||
</Dialog>
|
||||
<Dialog
|
||||
ref={nodeRef}
|
||||
open={props.open}
|
||||
onClick={dialogOnClick}
|
||||
onClose={handleOnClose}
|
||||
scroll="paper"
|
||||
maxWidth={props.maxWidth}
|
||||
fullWidth={true}
|
||||
style={{...props.style, ...draggedState.style}}
|
||||
disableEnforceFocus={true}
|
||||
disablePortal={false}
|
||||
hideBackdrop={draggedState.hideBackdrop}
|
||||
aria-labelledby="scroll-dialog-title"
|
||||
aria-describedby="scroll-dialog-description"
|
||||
sx={{
|
||||
[".MuiPaper-root"]: {
|
||||
...draggedState.paperStyle
|
||||
},
|
||||
[".MuiDialog-container"]: {
|
||||
...draggedState.containerStyle
|
||||
}
|
||||
}}
|
||||
onMouseDown={dialogOnClick}
|
||||
onContextMenu={dialogOnContextMenu}
|
||||
>
|
||||
{props.innerDialog}
|
||||
</Dialog>
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
export function MythicModifyStringDialog(props) {
|
||||
const [comment, setComment] = React.useState("");
|
||||
const [wrap, setWrap] = React.useState(props.wrap ? props.wrap : false);
|
||||
const theme = useTheme();
|
||||
const onCommitSubmit = () => {
|
||||
props.onSubmit(comment);
|
||||
if(props.dontCloseOnSubmit){
|
||||
return;
|
||||
}
|
||||
props.onClose();
|
||||
}
|
||||
const onChange = (name, value, error) => {
|
||||
const onChange = (value) => {
|
||||
setComment(value);
|
||||
}
|
||||
useEffect( () => {
|
||||
setComment(props.value);
|
||||
try{
|
||||
setComment(JSON.stringify(JSON.parse(props.value), null, 2));
|
||||
}catch(error){
|
||||
setComment(props.value);
|
||||
}
|
||||
|
||||
}, [props.value]);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">{props.title}</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
<MythicTextField autoFocus onEnter={props?.onEnter || onCommitSubmit} onChange={onChange} value={comment} multiline={props?.multiline || false} maxRows={props.maxRows} />
|
||||
{props.title !== "" &&
|
||||
<MythicDraggableDialogTitle>{props.title}
|
||||
<MythicStyledTooltip title={wrap ? "Toggle off word wrap" : "Toggl on word wrap"}
|
||||
tooltipStyle={{float: "right"}}>
|
||||
<IconButton onClick={() => {setWrap(!wrap)}}>
|
||||
<WrapTextIcon color={wrap ? "success" : "secondary"} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</MythicDraggableDialogTitle>
|
||||
}
|
||||
<DialogContent dividers={true} style={{ margin: 0, padding: 0}}>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
|
||||
width="100%"
|
||||
fontSize={14}
|
||||
showPrintMargin={false}
|
||||
wrapEnabled={wrap}
|
||||
value={comment}
|
||||
focus={true}
|
||||
onChange={onChange}
|
||||
setOptions={{
|
||||
tabSize: 4,
|
||||
useWorker: false,
|
||||
showInvisibles: false,
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
Close
|
||||
</Button>
|
||||
<Button onClick={onCommitSubmit} variant="contained" color="success">
|
||||
Submit
|
||||
</Button>
|
||||
</DialogActions>
|
||||
{ (props.onClose || props.onSubmit) &&
|
||||
<DialogActions>
|
||||
{props.onClose &&
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
Close
|
||||
</Button>
|
||||
}
|
||||
{props.onSubmit &&
|
||||
<Button onClick={onCommitSubmit} variant="contained" color="success">
|
||||
{props.onSubmitText ? props.onSubmitText : "Submit"}
|
||||
</Button>
|
||||
}
|
||||
</DialogActions>
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -92,7 +221,7 @@ export function MythicViewJSONAsTableDialog(props) {
|
||||
permissionDict = JSON.parse(props.value);
|
||||
}
|
||||
|
||||
if(permissionDict.constructor === Object){
|
||||
if(!Array.isArray(permissionDict) && typeof permissionDict !== 'string'){
|
||||
for(let key in permissionDict){
|
||||
if(permissionDict[key] && permissionDict[key].constructor === Object){
|
||||
// potentially have a nested dictionary here or array to become a dictionary, mark it
|
||||
@@ -145,14 +274,14 @@ export function MythicViewJSONAsTableDialog(props) {
|
||||
}, [props.value, props.leftColumn, props.rightColumn]);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title" style={{wordBreak: "break-all", maxWidth: "100%"}}>{props.title}</DialogTitle>
|
||||
<Paper elevation={5} style={{position: "relative"}} variant={"elevation"}>
|
||||
<TableContainer className="mythicElement">
|
||||
<MythicDraggableDialogTitle style={{wordBreak: "break-all", maxWidth: "100%"}}>{props.title}</MythicDraggableDialogTitle>
|
||||
|
||||
<TableContainer className="mythicElement" style={{paddingLeft: "10px"}}>
|
||||
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{headers.map( (header, index) => (
|
||||
<TableCell key={'header' + index} style={index === 0 ? {width: "30%", wordBreak: "break-all"} : {wordBreak: "break-all"}}>{header}</TableCell>
|
||||
<TableCell key={'header' + index} style={index === 0 ? {width: "15%", wordBreak: "break-all"} : {wordBreak: "break-all"}}>{header}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
@@ -168,7 +297,7 @@ export function MythicViewJSONAsTableDialog(props) {
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{element.headers.map( (header, index) => (
|
||||
<TableCell key={'eheader' + header + index} style={index === 0 ? {width: "30%", wordBreak: "break-all"} : {wordBreak: "break-all"}}>{header}</TableCell>
|
||||
<TableCell key={'eheader' + header + index} style={index === 0 ? {width: "15%", wordBreak: "break-all"} : {wordBreak: "break-all"}}>{header}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
@@ -177,14 +306,14 @@ export function MythicViewJSONAsTableDialog(props) {
|
||||
Object.keys(element.value).map( (key, dictIndex) => (
|
||||
<TableRow key={'element' + dictIndex + "dictheader"}>
|
||||
<TableCell style={{width: "30%", wordBreak: "break-all"}}>{key}</TableCell>
|
||||
<TableCell style={{wordBreak: "break-all"}}>{convertValueToContextValue(key, element.value[key], props.me)}</TableCell>
|
||||
<TableCell style={{wordBreak: "break-all", whiteSpace: "pre-wrap"}}>{convertValueToContextValue(key, element.value[key], props.me)}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
): (
|
||||
element.value.map( (e, elementIndex) => (
|
||||
<TableRow>
|
||||
{element.headers.map( (header, headerIndex) => (
|
||||
<TableCell key={'element' + elementIndex + "header" + headerIndex} style={headerIndex === 0 ? {width: "30%", wordBreak: "break-all"} : {wordBreak: "break-all"}}>{convertValueToContextValue(header, e[header], props.me)}</TableCell>
|
||||
<TableCell key={'element' + elementIndex + "header" + headerIndex} style={headerIndex === 0 ? {width: "15%", wordBreak: "break-all"} : {wordBreak: "break-all", whiteSpace: "pre-wrap"}}>{convertValueToContextValue(header, e[header], props.me)}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
@@ -212,7 +341,6 @@ export function MythicViewJSONAsTableDialog(props) {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Paper>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
Close
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
import React from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import {MythicDialog} from './MythicDialog';
|
||||
import MythicTextField from './MythicTextField';
|
||||
import {MythicDialog, MythicModifyStringDialog} from './MythicDialog';
|
||||
|
||||
export function MythicDisplayTextDialog(props) {
|
||||
return (
|
||||
<MythicDialog fullWidth={props.fullWidth === null ? false : props.fullWidth} maxWidth={props.maxWidth === null ? "sm" : props.maxWidth} open={props.open} onClose={()=>{props.onClose()}} innerDialog={
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">{props.title}</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
<MythicTextField multiline={true} value={props.value} onChange={()=>{}} />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</React.Fragment>
|
||||
<MythicDialog fullWidth={props.fullWidth === null ? false : props.fullWidth}
|
||||
maxWidth={props.maxWidth === null ? "sm" : props.maxWidth}
|
||||
open={props.open}
|
||||
onClose={()=>{props.onClose()}} innerDialog={
|
||||
<MythicModifyStringDialog title={props.title}
|
||||
onClose={props.onClose}
|
||||
value={props.value}
|
||||
maxRows={20} />
|
||||
} />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
|
||||
|
||||
export const MythicDraggableDialogTitle = ({children}) => {
|
||||
return (
|
||||
<DialogTitle id="mythic-draggable-title" style={{ cursor: 'move', width: "100%" }}>
|
||||
{children}
|
||||
</DialogTitle>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
export const reorder = (
|
||||
list,
|
||||
startIndex,
|
||||
endIndex
|
||||
) => {
|
||||
const result = Array.from(list);
|
||||
const [removed] = result.splice(startIndex, 1);
|
||||
result.splice(endIndex, 0, removed);
|
||||
return result;
|
||||
};
|
||||
@@ -79,7 +79,7 @@ return (
|
||||
Send a feedback report to the slack webhook configured for the current operation.
|
||||
This provides a way to easily capture a bug, feedback requests, or comments without breaking operator flow too much. <br/>
|
||||
Shift+Enter will auto-submit the form.
|
||||
<TableContainer component={Paper} className="mythicElement">
|
||||
<TableContainer className="mythicElement">
|
||||
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
<TableBody style={{whiteSpace: "pre"}}>
|
||||
<TableRow hover >
|
||||
|
||||
@@ -12,14 +12,8 @@ import ErrorIcon from '@mui/icons-material/Error';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Typography } from '@mui/material';
|
||||
import { MythicStyledTooltip } from "./MythicStyledTooltip";
|
||||
import Grow from '@mui/material/Grow';
|
||||
import Popper from '@mui/material/Popper';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import MenuList from '@mui/material/MenuList';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import WidgetsIcon from '@mui/icons-material/Widgets';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import { areEqual } from 'react-window';
|
||||
|
||||
const PREFIX = 'FileBrowserVirtualTree';
|
||||
|
||||
@@ -145,22 +139,32 @@ const StyledAutoSizer = styled(AutoSizer)((
|
||||
width: 1,
|
||||
}
|
||||
}));
|
||||
|
||||
const VirtualTreeRow = ({
|
||||
function itemKey(index, data) {
|
||||
// Find the item at the specified index.
|
||||
// In this case "data" is an Array that was passed to List as "itemData".
|
||||
const item = data[index];
|
||||
if(item.root){
|
||||
return `${item.group};${item.id}`;
|
||||
}
|
||||
if(item.is_group){
|
||||
return item.group;
|
||||
}
|
||||
return `${item.group};${item.host};${item.full_path_text}`;
|
||||
}
|
||||
const VirtualTreeRow = React.memo(({
|
||||
onSelectNode,
|
||||
onExpandNode,
|
||||
onCollapseNode,
|
||||
onDoubleClickNode,
|
||||
contextMenuOptions,
|
||||
onContextMenu,
|
||||
tabInfo,
|
||||
selectedFolderData,
|
||||
...ListProps
|
||||
}) => {
|
||||
const itemTreeData = ListProps.data[ListProps.index];
|
||||
const item = ListProps.treeRootData[itemTreeData.group]?.[itemTreeData.host]?.[itemTreeData.full_path_text] || itemTreeData;
|
||||
//console.log("item", item, "itemlookup", ListProps.treeRootData[itemTreeData.host]?.[itemTreeData.name])
|
||||
const dropdownAnchorRef = React.useRef(null);
|
||||
const theme = useTheme();
|
||||
|
||||
const handleOnClickButton = (e) => {
|
||||
e.stopPropagation();
|
||||
if (itemTreeData.isOpen) {
|
||||
@@ -173,42 +177,25 @@ const VirtualTreeRow = ({
|
||||
const handleOnClickRow = (e) => {
|
||||
onSelectNode(item.id, {...item, group: itemTreeData.group, host: itemTreeData.host});
|
||||
};
|
||||
const [openContextMenu, setOpenContextMenu] = React.useState(false);
|
||||
|
||||
const handleContextClick = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if(item.root){
|
||||
|
||||
}else if(item.is_group){
|
||||
|
||||
}else {
|
||||
if(contextMenuOptions && contextMenuOptions.length > 0){
|
||||
setOpenContextMenu(true);
|
||||
}
|
||||
const handleContextClick = (e) => {
|
||||
onContextMenu({event: e, item, itemTreeData});
|
||||
}
|
||||
const selectedPath = () => {
|
||||
if(itemTreeData.group === selectedFolderData.group && itemTreeData.host === selectedFolderData.host){
|
||||
if(itemTreeData.root){
|
||||
return "selectedCallbackHierarchy";
|
||||
}
|
||||
if(selectedFolderData.full_path_text === itemTreeData.full_path_text){
|
||||
return "selectedCallback";
|
||||
}
|
||||
|
||||
},
|
||||
[contextMenuOptions] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
);
|
||||
const handleMenuItemClick = (event, index, callback_id, callback_display_id) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
contextMenuOptions[index].click({event,
|
||||
node: {...item, group: itemTreeData.group, host: itemTreeData.host},
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
setOpenContextMenu(false);
|
||||
};
|
||||
const handleClose = (event) => {
|
||||
if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
setOpenContextMenu(false);
|
||||
};
|
||||
return "";
|
||||
}
|
||||
return (
|
||||
<div className={"hoverme"} style={ListProps.style} onContextMenu={handleContextClick} ref={dropdownAnchorRef} onClick={handleOnClickRow}>
|
||||
<div className={`hoverme ${selectedPath()}`}
|
||||
style={ListProps.style}
|
||||
onContextMenu={handleContextClick}
|
||||
onClick={handleOnClickRow}>
|
||||
<div style={{display: 'flex' , marginBottom: "1px", flexGrow: 1, width: "100%"}}>
|
||||
{[...Array(itemTreeData.depth)].map((o, i) => (
|
||||
<div
|
||||
@@ -225,79 +212,52 @@ const VirtualTreeRow = ({
|
||||
style={{ backgroundColor: theme.body, color: theme.text, alignItems: 'center', display: 'flex', paddingRight: "10px", textDecoration: itemTreeData.deleted ? 'line-through' : '' }}
|
||||
|
||||
>
|
||||
<Popper open={openContextMenu} anchorEl={dropdownAnchorRef.current} role={undefined} transition style={{zIndex: 4}}>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin: placement === 'bottom' ? 'left top' : 'left bottom',
|
||||
}}
|
||||
>
|
||||
<Paper variant="outlined" className={"dropdownMenuColored"}>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<MenuList id="split-button-menu" style={{paddingTop: 0}} >
|
||||
<ListSubheader component={"li"} className={"MuiListSubheader-root"}>
|
||||
Act from current Callback: {tabInfo["displayID"]}
|
||||
</ListSubheader>
|
||||
{contextMenuOptions.map((option, index) => (
|
||||
<MenuItem
|
||||
key={option.name + index}
|
||||
onClick={(event) => handleMenuItemClick(event, index, tabInfo["callbackID"], tabInfo["displayID"])}
|
||||
>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
{
|
||||
item?.callback && item?.["callback"]?.["id"] !== tabInfo["callbackID"] &&
|
||||
<ListSubheader component={"li"} className={"MuiListSubheader-root"}>
|
||||
Act from originating Callback: {item?.callback?.["display_id"] || tabInfo["displayID"]}
|
||||
</ListSubheader>
|
||||
}
|
||||
{
|
||||
item?.callback && item?.["callback"]?.["id"] !== tabInfo["callbackID"] &&
|
||||
contextMenuOptions.map((option, index) => (
|
||||
<MenuItem
|
||||
key={option.name + index}
|
||||
onClick={(event) => handleMenuItemClick(event, index,
|
||||
item?.["callback"]?.["id"] || tabInfo["callbackID"],
|
||||
item?.["callback"]?.["display_id"] || tabInfo["displayID"])}
|
||||
>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))
|
||||
}
|
||||
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
{itemTreeData.is_group ? (
|
||||
<WidgetsIcon style={{marginLeft: "3px", marginRight: "5px" }} />
|
||||
<WidgetsIcon style={{
|
||||
width: "15px",
|
||||
height: "15px",
|
||||
marginLeft: "3px",
|
||||
marginRight: "5px" }} />
|
||||
): itemTreeData.root ? (
|
||||
<ComputerIcon style={{ marginLeft: '3px', marginRight: '5px' }} />
|
||||
<ComputerIcon style={{
|
||||
width: "15px",
|
||||
height: "15px",
|
||||
marginLeft: '3px',
|
||||
marginRight: '5px' }} />
|
||||
) : !itemTreeData.can_have_children ? (
|
||||
<DescriptionIcon style={{ marginLeft: '3px', marginRight: '5px' }} />
|
||||
<DescriptionIcon style={{
|
||||
width: "15px",
|
||||
height: "15px",
|
||||
marginLeft: '3px',
|
||||
marginRight: '5px' }} />
|
||||
) : itemTreeData.isOpen ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faFolderOpen}
|
||||
style={{
|
||||
width: "15px",
|
||||
height: "15px",
|
||||
marginLeft: '3px',
|
||||
marginRight: '5px',
|
||||
color: item?.metadata?.has_children ? theme.folderColor : theme.palette.text.secondary,
|
||||
}}
|
||||
size={"1x"}
|
||||
color: item?.has_children || item.success ? theme.folderColor : theme.emptyFolderColor,
|
||||
}}
|
||||
size={"lg"}
|
||||
onClick={handleOnClickButton} />
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
style={{ paddingTop: '5px', marginLeft: '3px', marginRight: '5px', color: item?.metadata?.has_children ? theme.folderColor : theme.palette.text.secondary, }}
|
||||
size={"lg"} icon={faFolder} onClick={handleOnClickButton} />
|
||||
style={{
|
||||
width: "15px",
|
||||
height: "15px",
|
||||
marginLeft: '3px',
|
||||
marginRight: '5px',
|
||||
color: item?.has_children || item.success ? theme.folderColor : theme.emptyFolderColor, }}
|
||||
size={"lg"}
|
||||
icon={faFolder} onClick={handleOnClickButton} />
|
||||
)}
|
||||
<Typography
|
||||
style={{
|
||||
color:
|
||||
item?.metadata?.has_children ||
|
||||
item?.has_children ||
|
||||
item.success !== null
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary,
|
||||
@@ -306,11 +266,11 @@ const VirtualTreeRow = ({
|
||||
</Typography>
|
||||
|
||||
{item.success === true && itemTreeData.depth > 0 ? (
|
||||
<MythicStyledTooltip title='Successfully listed contents of folder'>
|
||||
<MythicStyledTooltip title='Successfully listed contents of folder' style={{display: "inline-flex", marginLeft: "5px"}}>
|
||||
<CheckCircleOutlineIcon fontSize='small' color="success" />
|
||||
</MythicStyledTooltip>
|
||||
) : item.success === false && itemTreeData.depth > 0 ? (
|
||||
<MythicStyledTooltip title='Failed to list contents of folder'>
|
||||
<MythicStyledTooltip title='Failed to list contents of folder' style={{display: "inline-flex", marginLeft: "5px"}}>
|
||||
<ErrorIcon fontSize='small' color="error" />
|
||||
</MythicStyledTooltip>
|
||||
) : null}
|
||||
@@ -319,19 +279,28 @@ const VirtualTreeRow = ({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FileBrowserVirtualTree = ({
|
||||
}, areEqual);
|
||||
const caseInsensitiveCompare = (a, b) => {
|
||||
try{
|
||||
return a.localeCompare(b);
|
||||
}catch(error){
|
||||
console.log("localeCompare failed for", a, b);
|
||||
return a < b;
|
||||
}
|
||||
}
|
||||
const FileBrowserVirtualTreePreMemo = ({
|
||||
treeRootData,
|
||||
treeAdjMatrix,
|
||||
openNodes,
|
||||
onSelectNode,
|
||||
onExpandNode,
|
||||
onCollapseNode,
|
||||
contextMenuOptions,
|
||||
onContextMenu,
|
||||
showDeletedFiles,
|
||||
selectedFolderData,
|
||||
tabInfo,
|
||||
}) => {
|
||||
const gridRef = React.useRef(null);
|
||||
const flattenNode = useCallback(
|
||||
// node is just a full_path_text
|
||||
(node, group, host, depth = 0) => {
|
||||
@@ -339,26 +308,26 @@ const FileBrowserVirtualTree = ({
|
||||
if(depth === 0){
|
||||
return [
|
||||
{
|
||||
id: treeRootData[group][host][node].id,
|
||||
name: treeRootData[group][host][node].name_text,
|
||||
full_path_text: treeRootData[group][host][node].full_path_text,
|
||||
deleted: treeRootData[group][host][node].deleted,
|
||||
id: treeRootData[group]?.[host]?.[node].id,
|
||||
name: treeRootData[group]?.[host]?.[node].name_text,
|
||||
full_path_text: treeRootData[group]?.[host]?.[node].full_path_text,
|
||||
deleted: treeRootData[group]?.[host]?.[node].deleted,
|
||||
depth,
|
||||
isLeaf: Object.keys(treeAdjMatrix[group][host]?.[node] || {}).length === 0,
|
||||
can_have_children: treeRootData[group][host][node].can_have_children,
|
||||
isLeaf: Object.keys(treeAdjMatrix[group]?.[host]?.[node] || {}).length === 0,
|
||||
can_have_children: treeRootData[group]?.[host]?.[node].can_have_children,
|
||||
isOpen: true,
|
||||
children: (treeAdjMatrix[group][host]?.[node] || {}),
|
||||
host,
|
||||
group,
|
||||
root: true
|
||||
},
|
||||
...(Object.keys(treeAdjMatrix[group][host]?.[node] || {})).reduce( (prev, cur) => {
|
||||
...(Object.keys(treeAdjMatrix[group]?.[host]?.[node] || {})).sort(caseInsensitiveCompare).reduce( (prev, cur) => {
|
||||
if(!treeRootData[group][host][cur].can_have_children){return [...prev]}
|
||||
return [...prev, flattenNode(cur, group, host, depth+1)];
|
||||
}, []).flat()
|
||||
];
|
||||
}
|
||||
if (openNodes[treeRootData[group][host][node].id] === true) {
|
||||
if (openNodes[`${group};${host};${treeRootData[group][host][node].full_path_text}`] === true) {
|
||||
return [
|
||||
{
|
||||
id: treeRootData[group][host][node].id,
|
||||
@@ -366,15 +335,15 @@ const FileBrowserVirtualTree = ({
|
||||
full_path_text: treeRootData[group][host][node].full_path_text,
|
||||
deleted: treeRootData[group][host][node].deleted,
|
||||
depth,
|
||||
isLeaf: Object.keys(treeAdjMatrix[group][host]?.[node] || {}).length === 0,
|
||||
isLeaf: Object.keys(treeAdjMatrix[group]?.[host]?.[node] || {}).length === 0,
|
||||
can_have_children: treeRootData[group][host][node].can_have_children,
|
||||
isOpen: true,
|
||||
children: (treeAdjMatrix[group][host]?.[node] || {}),
|
||||
children: (treeAdjMatrix[group]?.[host]?.[node] || {}),
|
||||
host,
|
||||
group,
|
||||
root: false,
|
||||
},
|
||||
...(Object.keys(treeAdjMatrix[group][host]?.[node] || {})).reduce( (prev, cur) => {
|
||||
...(Object.keys(treeAdjMatrix[group]?.[host]?.[node] || {})).sort(caseInsensitiveCompare).reduce( (prev, cur) => {
|
||||
if(!treeRootData[group][host][cur].can_have_children){return [...prev]}
|
||||
if(!showDeletedFiles && treeRootData[group][host][cur].deleted){return [...prev]}
|
||||
return [...prev, flattenNode(cur, group, host, depth+1)];
|
||||
@@ -383,13 +352,13 @@ const FileBrowserVirtualTree = ({
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: treeRootData[group][host][node].id,
|
||||
name: treeRootData[group][host][node].name_text,
|
||||
full_path_text: treeRootData[group][host][node].full_path_text,
|
||||
deleted: treeRootData[group][host][node].deleted,
|
||||
id: treeRootData[group]?.[host]?.[node].id,
|
||||
name: treeRootData[group]?.[host]?.[node].name_text,
|
||||
full_path_text: treeRootData[group]?.[host]?.[node].full_path_text,
|
||||
deleted: treeRootData[group]?.[host]?.[node].deleted,
|
||||
depth,
|
||||
isLeaf: Object.keys(treeAdjMatrix[group]?.[host]?.[node] || {}).length === 0,
|
||||
can_have_children: treeRootData[group][host][node].can_have_children,
|
||||
can_have_children: treeRootData[group]?.[host]?.[node].can_have_children,
|
||||
isOpen: false,
|
||||
children: (treeAdjMatrix[group]?.[host]?.[node] || {}),
|
||||
host,
|
||||
@@ -399,15 +368,14 @@ const FileBrowserVirtualTree = ({
|
||||
];
|
||||
|
||||
},
|
||||
[openNodes, showDeletedFiles] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
[openNodes, showDeletedFiles, treeAdjMatrix] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
);
|
||||
|
||||
const flattenedNodes = useMemo(() => {
|
||||
//console.log("in tree", treeRootData, treeAdjMatrix)
|
||||
// need to return an array
|
||||
let finalData = [];
|
||||
//console.log(treeAdjMatrix);
|
||||
const groupKeys = Object.keys(treeAdjMatrix).sort();
|
||||
const groupKeys = Object.keys(treeAdjMatrix).sort(caseInsensitiveCompare);
|
||||
for(let i = 0; i < groupKeys.length; i++){
|
||||
finalData.push({
|
||||
id: groupKeys[i],
|
||||
@@ -424,7 +392,7 @@ const FileBrowserVirtualTree = ({
|
||||
children: treeAdjMatrix[groupKeys[i]],
|
||||
full_path_text: groupKeys[i],
|
||||
});
|
||||
const hostKeys = Object.keys(treeAdjMatrix[groupKeys[i]]).sort();
|
||||
const hostKeys = Object.keys(treeAdjMatrix[groupKeys[i]]).sort(caseInsensitiveCompare);
|
||||
for(let j = 0; j < hostKeys.length; j++){
|
||||
//for(const [host, matrix] of Object.entries(hosts)){
|
||||
finalData.push({
|
||||
@@ -442,7 +410,7 @@ const FileBrowserVirtualTree = ({
|
||||
full_path_text: hostKeys[j],
|
||||
});
|
||||
//console.log(matrix);
|
||||
finalData.push(...Object.keys(treeAdjMatrix[groupKeys[i]][hostKeys[j]][""]).reduce((prev, c) => {
|
||||
finalData.push(...Object.keys(treeAdjMatrix[groupKeys[i]][hostKeys[j]][""]).sort(caseInsensitiveCompare).reduce((prev, c) => {
|
||||
if(!showDeletedFiles && c.deleted) {
|
||||
return [...prev];
|
||||
} else {
|
||||
@@ -451,31 +419,44 @@ const FileBrowserVirtualTree = ({
|
||||
}, []).flat())
|
||||
}
|
||||
}
|
||||
|
||||
//console.log("flattened data", finalData)
|
||||
return finalData;
|
||||
//nodes.map((node) => flattenNode(node)).flat()
|
||||
},[flattenNode, treeRootData, treeAdjMatrix, showDeletedFiles]);
|
||||
React.useEffect( () => {
|
||||
let rowIndex = flattenedNodes?.findIndex(e =>
|
||||
e.full_path_text === selectedFolderData.full_path_text &&
|
||||
e.host === selectedFolderData.host &&
|
||||
e.group === selectedFolderData.group
|
||||
);
|
||||
if(rowIndex >= 0){
|
||||
if(gridRef.current){
|
||||
gridRef.current?.scrollToItem(rowIndex, "smart")
|
||||
}
|
||||
}
|
||||
}, [selectedFolderData, flattenedNodes]);
|
||||
return flattenedNodes.length > 0 ? (
|
||||
<StyledAutoSizer>
|
||||
{(AutoSizerProps) => (
|
||||
<List
|
||||
itemData={flattenedNodes}
|
||||
direction="vertical"
|
||||
layout="vertical"
|
||||
height={AutoSizerProps.height}
|
||||
width={AutoSizerProps.width}
|
||||
itemCount={flattenedNodes.length}
|
||||
itemKey={itemKey}
|
||||
itemSize={24}
|
||||
ref={gridRef}
|
||||
>
|
||||
{(ListProps) => (
|
||||
<VirtualTreeRow
|
||||
{...ListProps}
|
||||
tabInfo={tabInfo}
|
||||
selectedFolderData={selectedFolderData}
|
||||
treeRootData={treeRootData}
|
||||
onSelectNode={onSelectNode}
|
||||
onExpandNode={onExpandNode}
|
||||
onCollapseNode={onCollapseNode}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
onContextMenu={onContextMenu}
|
||||
/>
|
||||
)}
|
||||
</List>
|
||||
@@ -483,5 +464,4 @@ const FileBrowserVirtualTree = ({
|
||||
</StyledAutoSizer>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default FileBrowserVirtualTree;
|
||||
export const FileBrowserVirtualTree = React.memo(FileBrowserVirtualTreePreMemo);
|
||||
@@ -1,122 +1,100 @@
|
||||
import React from 'react';
|
||||
import {previewFileQuery} from "../pages/Search/FileMetaTable";
|
||||
import {snackActions} from "../utilities/Snackbar";
|
||||
import {MythicStyledTooltip} from "./MythicStyledTooltip";
|
||||
import hexFile from "../../assets/file_bin.png";
|
||||
import txtFile from "../../assets/file_txt.png";
|
||||
import {MythicDialog} from "./MythicDialog";
|
||||
import {PreviewFileStringDialog} from "../pages/Search/PreviewFileStringDialog";
|
||||
import {PreviewFileHexDialog} from "../pages/Search/PreviewFileHexDialog";
|
||||
import {Link} from '@mui/material';
|
||||
import {useMutation, useLazyQuery, gql} from '@apollo/client';
|
||||
import {b64DecodeUnicode} from "../pages/Callbacks/ResponseDisplay";
|
||||
import {PreviewFileMediaDialog} from "../pages/Search/PreviewFileMedia";
|
||||
import {faPhotoVideo} from '@fortawesome/free-solid-svg-icons';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {useMythicLazyQuery} from "../utilities/useMythicLazyQuery";
|
||||
|
||||
export const getfileInformationQuery = gql`
|
||||
query getFileInformation($file_id: String!){
|
||||
filemeta(where: {agent_file_id: {_eq: $file_id}}){
|
||||
filename_text
|
||||
agent_file_id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const MythicFileContext = ({agent_file_id, display_link, filename}) => {
|
||||
export const MythicFileContext = ({agent_file_id, display_link, filename, extraStyles}) => {
|
||||
const [fileData, setFileData] = React.useState({
|
||||
agent_file_id: agent_file_id,
|
||||
display_link: display_link,
|
||||
filename: filename,
|
||||
})
|
||||
const [openPreviewStringsDialog, setOpenPreviewStringsDialog] = React.useState(false);
|
||||
const [openPreviewHexDialog, setOpenPreviewHexDialog] = React.useState(false);
|
||||
const [fileContents, setFileContents] = React.useState('');
|
||||
const [previewFileString] = useMutation(previewFileQuery, {
|
||||
onCompleted: (data) => {
|
||||
if(data.previewFile.status === "success"){
|
||||
setFileContents(data.previewFile.contents);
|
||||
setOpenPreviewStringsDialog(true);
|
||||
}else{
|
||||
snackActions.error(data.previewFile.error)
|
||||
}
|
||||
},
|
||||
onError: (data) => {
|
||||
console.log(data);
|
||||
snackActions.error(data);
|
||||
}
|
||||
});
|
||||
const [previewFileHex] = useMutation(previewFileQuery, {
|
||||
onCompleted: (data) => {
|
||||
if(data.previewFile.status === "success"){
|
||||
setFileContents(data.previewFile.contents);
|
||||
setOpenPreviewHexDialog(true);
|
||||
}else{
|
||||
snackActions.error(data.previewFile.error)
|
||||
}
|
||||
},
|
||||
onError: (data) => {
|
||||
console.log(data);
|
||||
snackActions.error(data);
|
||||
}
|
||||
});
|
||||
const onPreviewStrings = (event) => {
|
||||
const [openPreviewMediaDialog, setOpenPreviewMediaDialog] = React.useState(false);
|
||||
const onPreviewMedia = (event) => {
|
||||
if(event){
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
previewFileString({variables: {file_id: fileData.agent_file_id}})
|
||||
setOpenPreviewMediaDialog(true);
|
||||
}
|
||||
const onPreviewHex = (event) => {
|
||||
if(event){
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const getFileInformationSuccess = (data) => {
|
||||
if(data.filemeta.length === 0){
|
||||
setFileData({
|
||||
filename: undefined,
|
||||
display_link: "",
|
||||
agent_file_id: agent_file_id,
|
||||
})
|
||||
return;
|
||||
}
|
||||
setFileData( {...fileData,
|
||||
agent_file_id: data.filemeta[0].agent_file_id,
|
||||
filename: b64DecodeUnicode(data.filemeta[0].filename_text)});
|
||||
if(display_link === "" || display_link === undefined){
|
||||
setFileData( {...fileData,
|
||||
agent_file_id: data.filemeta[0].agent_file_id,
|
||||
filename: b64DecodeUnicode(data.filemeta[0].filename_text),
|
||||
display_link: b64DecodeUnicode(data.filemeta[0].filename_text)});
|
||||
}
|
||||
previewFileHex({variables: {file_id: fileData.agent_file_id}})
|
||||
}
|
||||
const [getFileInformation] = useLazyQuery(getfileInformationQuery, {
|
||||
onCompleted: (data) => {
|
||||
setFileData( {...fileData, filename: b64DecodeUnicode(data.filemeta[0].filename_text)});
|
||||
if(display_link === "" || display_link === undefined){
|
||||
setFileData( {...fileData,
|
||||
filename: b64DecodeUnicode(data.filemeta[0].filename_text),
|
||||
display_link: b64DecodeUnicode(data.filemeta[0].filename_text)});
|
||||
}
|
||||
},
|
||||
onError: (data) => {
|
||||
snackActions.error("Failed to fetch instance data: " + data);
|
||||
console.log(data);
|
||||
},
|
||||
const getFileInformationError = (data) => {
|
||||
snackActions.error("Failed to fetch file data: " + data);
|
||||
console.log(data);
|
||||
}
|
||||
const getFileInformation = useMythicLazyQuery(getfileInformationQuery, {
|
||||
fetchPolicy: "no-cache"
|
||||
})
|
||||
React.useEffect( () => {
|
||||
if(filename === "" || filename === undefined){
|
||||
getFileInformation({variables: {file_id: fileData.agent_file_id}})
|
||||
if(agent_file_id !== '' && agent_file_id !== undefined){
|
||||
getFileInformation({variables: {file_id: agent_file_id}})
|
||||
.then(({data}) => getFileInformationSuccess(data)).catch((data) => getFileInformationError(data));
|
||||
}
|
||||
}, [filename]);
|
||||
}, [filename, agent_file_id]);
|
||||
if(agent_file_id === '' || agent_file_id === undefined){
|
||||
return null
|
||||
}
|
||||
if(fileData.agent_file_id === '' || fileData.filename === undefined){
|
||||
return agent_file_id
|
||||
}
|
||||
const onClose = (e) => {
|
||||
if(e){
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
setOpenPreviewMediaDialog(false)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<MythicStyledTooltip title={"Preview HEX XXD"}>
|
||||
<img src={hexFile} alt={"preview hex"} style={{height: "35px", cursor: "pointer"}}
|
||||
onClick={onPreviewHex}/>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Preview Strings"}>
|
||||
<img src={txtFile} alt={"preview strings"} style={{height: "35px", cursor: "pointer"}}
|
||||
onClick={onPreviewStrings} />
|
||||
<MythicStyledTooltip title={"Preview Media"} tooltipStyle={extraStyles ? extraStyles : {}}>
|
||||
<FontAwesomeIcon icon={faPhotoVideo}
|
||||
style={{height: "20px", position: "relative", cursor: "pointer", display: "inline-block"}}
|
||||
onClick={onPreviewMedia} />
|
||||
</MythicStyledTooltip>
|
||||
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" href={"/direct/download/" + fileData.agent_file_id}>
|
||||
{fileData.display_link === "" ? window.location.origin + "/direct/download/" + fileData.agent_file_id : fileData.display_link}
|
||||
</Link>
|
||||
{openPreviewStringsDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openPreviewStringsDialog}
|
||||
onClose={()=>{setOpenPreviewStringsDialog(false);}}
|
||||
innerDialog={<PreviewFileStringDialog onClose={()=>{setOpenPreviewStringsDialog(false);}}
|
||||
filename={fileData.filename} contents={fileContents}
|
||||
/>}
|
||||
/>
|
||||
}
|
||||
{openPreviewHexDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openPreviewHexDialog}
|
||||
onClose={()=>{setOpenPreviewHexDialog(false);}}
|
||||
innerDialog={<PreviewFileHexDialog onClose={()=>{setOpenPreviewHexDialog(false);}}
|
||||
filename={fileData.filename} contents={fileContents}
|
||||
/>}
|
||||
{openPreviewMediaDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openPreviewMediaDialog}
|
||||
onClose={onClose}
|
||||
innerDialog={<PreviewFileMediaDialog
|
||||
agent_file_id={fileData.agent_file_id}
|
||||
filename={fileData.filename}
|
||||
onClose={onClose} />}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
import { snackActions } from '../utilities/Snackbar';
|
||||
import {snackActions} from '../utilities/Snackbar';
|
||||
|
||||
export const UploadTaskFile = async (file, comment) => {
|
||||
let formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("comment", comment);
|
||||
snackActions.info("Uploading " + file.name + " to Mythic...", {autoHideDuration: 1000});
|
||||
try{
|
||||
formData.append("file", file);
|
||||
formData.append("comment", comment);
|
||||
snackActions.info("Uploading " + file.name + " to Mythic...", {autoHideDuration: 1000});
|
||||
}catch(error){
|
||||
console.log(error)
|
||||
return null;
|
||||
}
|
||||
try{
|
||||
const upload_response = await fetch('/api/v1.4/task_upload_file_webhook', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
|
||||
"Authorization": `Bearer ${localStorage.getItem("access_token")}`,
|
||||
MythicSource: "web",
|
||||
}
|
||||
});
|
||||
try{
|
||||
const upload_result = upload_response.json().then(data => {
|
||||
//console.log(data);
|
||||
return data?.agent_file_id || data?.error || null;
|
||||
}).catch(error => {
|
||||
console.log(upload_response);
|
||||
snackActions.warning("Error: " + upload_response.statusText + "\nError Code: " + upload_response.status);
|
||||
console.log("Error trying to get json response", error.toString());
|
||||
return null;
|
||||
@@ -32,4 +36,68 @@ export const UploadTaskFile = async (file, comment) => {
|
||||
snackActions.error(error.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export const UploadEventFile = async (file, comment) => {
|
||||
let formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("comment", comment);
|
||||
snackActions.info("Uploading " + file.name + " to Mythic...", {autoHideDuration: 1000});
|
||||
try{
|
||||
const upload_response = await fetch('/api/v1.4/eventing_import_webhook', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
|
||||
}
|
||||
});
|
||||
try{
|
||||
return upload_response.json().then(data => {
|
||||
//console.log(data);
|
||||
return data;
|
||||
}).catch(error => {
|
||||
console.log(upload_response);
|
||||
snackActions.warning("Error: " + upload_response.statusText + "\nError Code: " + upload_response.status);
|
||||
console.log("Error trying to get json response", error.toString());
|
||||
return null;
|
||||
});
|
||||
}catch(error){
|
||||
snackActions.error(error.toString());
|
||||
return null;
|
||||
}
|
||||
}catch(error){
|
||||
snackActions.error(error.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export const UploadEventGroupFile = async (file, eventgroup_id) => {
|
||||
let formData = new FormData();
|
||||
formData.append("eventgroup_id", eventgroup_id);
|
||||
formData.append("file", file);
|
||||
snackActions.info("Uploading " + file.name + " to Mythic...", {autoHideDuration: 1000});
|
||||
try{
|
||||
const upload_response = await fetch('/api/v1.4/eventing_register_file_webhook', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
|
||||
}
|
||||
});
|
||||
try{
|
||||
return upload_response.json().then(data => {
|
||||
//console.log(data);
|
||||
return data;
|
||||
}).catch(error => {
|
||||
console.log(upload_response);
|
||||
snackActions.warning("Error: " + upload_response.statusText + "\nError Code: " + upload_response.status);
|
||||
console.log("Error trying to get json response", error.toString());
|
||||
return null;
|
||||
});
|
||||
}catch(error){
|
||||
snackActions.error(error.toString());
|
||||
return null;
|
||||
}
|
||||
}catch(error){
|
||||
snackActions.error(error.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
import * as React from "react";
|
||||
import styled from "@emotion/styled";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import ArrowRight from "@mui/icons-material/ArrowRight";
|
||||
// slightly modified from https://medium.com/geekculture/creating-a-dropdown-with-nested-menu-items-using-react-mui-bb0c084226da
|
||||
export const Dropdown = React.forwardRef(
|
||||
(
|
||||
{
|
||||
menu,
|
||||
isOpen: controlledIsOpen,
|
||||
onOpen: onControlledOpen,
|
||||
externallyOpen,
|
||||
minWidth,
|
||||
absoluteX,
|
||||
absoluteY,
|
||||
anchorReference="anchorEl",
|
||||
style,
|
||||
transformOrigin,
|
||||
anchorOrigin,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [isInternalOpen, setInternalOpen] = React.useState(null);
|
||||
|
||||
const isOpen = controlledIsOpen || isInternalOpen;
|
||||
|
||||
let anchorRef = React.useRef(isOpen);
|
||||
if (ref) {
|
||||
anchorRef = ref;
|
||||
}
|
||||
const handleClose = (event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (anchorRef.current && anchorRef.current.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleForceClose();
|
||||
};
|
||||
const handleForceClose = () => {
|
||||
onControlledOpen ? onControlledOpen(null) : setInternalOpen(null);
|
||||
};
|
||||
const renderMenu = (menuItem, index) => {
|
||||
const { ...props } = menuItem.props;
|
||||
|
||||
let extraProps = {};
|
||||
if (props.menu) {
|
||||
extraProps = {
|
||||
parentMenuOpen: isOpen
|
||||
};
|
||||
}
|
||||
return React.createElement(menuItem.type, {
|
||||
...props,
|
||||
key: index,
|
||||
...extraProps,
|
||||
onClick: (event) => {
|
||||
event.stopPropagation();
|
||||
if (menuItem.props.onClick) {
|
||||
menuItem.props.onClick(event);
|
||||
}
|
||||
},
|
||||
children: props.menu
|
||||
? React.Children.map(props.menu, renderMenu)
|
||||
: props.children
|
||||
});
|
||||
};
|
||||
const anchorEl = isOpen && isOpen.currentTarget ? isOpen.currentTarget : isOpen;
|
||||
return (
|
||||
<>
|
||||
<Menu
|
||||
elevation={5}
|
||||
PaperProps={{ sx: { minWidth: minWidth ?? 0 } }}
|
||||
style={{zIndex: 100000, position: "absolute"}}
|
||||
anchorEl={anchorReference === "anchorEl" ? anchorEl : undefined}
|
||||
transition={"true"}
|
||||
open={!!externallyOpen}
|
||||
anchorPosition={anchorReference === "anchorEl" ? undefined : {top: absoluteY, left: absoluteX}}
|
||||
anchorReference={ anchorReference ? anchorReference : "anchorEl"}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={anchorOrigin ? anchorOrigin : {
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
transformOrigin={transformOrigin ? transformOrigin : {
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
{React.Children.map(menu, renderMenu)}
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const NestedMenuItem = React.forwardRef((props, ref) => {
|
||||
const {
|
||||
parentMenuOpen,
|
||||
label,
|
||||
rightIcon = <ArrowRight style={{ fontSize: 16 }} />,
|
||||
children,
|
||||
customTheme,
|
||||
className,
|
||||
tabIndex: tabIndexProp,
|
||||
ContainerProps: ContainerPropsProp = {},
|
||||
rightAnchored,
|
||||
...MenuItemProps
|
||||
} = props;
|
||||
|
||||
const { ref: containerRefProp, ...ContainerProps } = ContainerPropsProp;
|
||||
|
||||
const menuItemRef = React.useRef(null);
|
||||
React.useImperativeHandle(ref, () => menuItemRef.current);
|
||||
|
||||
const containerRef = React.useRef(null);
|
||||
React.useImperativeHandle(containerRefProp, () => containerRef.current);
|
||||
|
||||
const menuContainerRef = React.useRef(null);
|
||||
|
||||
const [isSubMenuOpen, setIsSubMenuOpen] = React.useState(false);
|
||||
|
||||
const handleMouseEnter = (event) => {
|
||||
setIsSubMenuOpen(true);
|
||||
|
||||
if (ContainerProps?.onMouseEnter) {
|
||||
ContainerProps.onMouseEnter(event);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = (event) => {
|
||||
setIsSubMenuOpen(false);
|
||||
|
||||
if (ContainerProps?.onMouseLeave) {
|
||||
ContainerProps.onMouseLeave(event);
|
||||
}
|
||||
};
|
||||
|
||||
const isSubmenuFocused = () => {
|
||||
const active = containerRef.current?.ownerDocument?.activeElement;
|
||||
|
||||
for (const child of menuContainerRef.current?.children ?? []) {
|
||||
if (child === active) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleFocus = (event) => {
|
||||
if (event.target === containerRef.current) {
|
||||
setIsSubMenuOpen(true);
|
||||
}
|
||||
|
||||
if (ContainerProps?.onFocus) {
|
||||
ContainerProps.onFocus(event);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === "Escape") {
|
||||
setIsSubMenuOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSubmenuFocused()) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
const active = containerRef.current?.ownerDocument?.activeElement;
|
||||
|
||||
if (event.key === "ArrowLeft" && isSubmenuFocused()) {
|
||||
containerRef.current?.focus();
|
||||
}
|
||||
|
||||
if (
|
||||
event.key === "ArrowRight" &&
|
||||
event.target === containerRef.current &&
|
||||
event.target === active
|
||||
) {
|
||||
const firstChild = menuContainerRef.current?.children[0];
|
||||
firstChild?.focus();
|
||||
}
|
||||
if(event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "ArrowLeft"){
|
||||
setIsSubMenuOpen(false);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
const open = isSubMenuOpen && parentMenuOpen;
|
||||
|
||||
let tabIndex;
|
||||
if (!props.disabled) {
|
||||
tabIndex = tabIndexProp !== undefined ? tabIndexProp : -1;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...ContainerProps}
|
||||
ref={containerRef}
|
||||
onFocus={handleFocus}
|
||||
tabIndex={tabIndex}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<MenuItem
|
||||
{...MenuItemProps}
|
||||
data-open={!!open || undefined}
|
||||
className={className}
|
||||
ref={menuItemRef}
|
||||
>
|
||||
{label}
|
||||
<div style={{ flexGrow: 1 }} />
|
||||
{rightIcon}
|
||||
</MenuItem>
|
||||
<Menu
|
||||
hideBackdrop
|
||||
disablePortal
|
||||
disableEnforceFocus={false}
|
||||
disableAutoFocus={false}
|
||||
style={{ pointerEvents: "none", zIndex: 100000}}
|
||||
anchorEl={menuItemRef.current}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: rightAnchored ? "left" : "right"
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: rightAnchored ? "right" : "left"
|
||||
}}
|
||||
css={customTheme}
|
||||
open={!!open}
|
||||
onClose={() => {
|
||||
setIsSubMenuOpen(false);
|
||||
}}
|
||||
MenuListProps={{
|
||||
'aria-hidden': false,
|
||||
role: 'menu'
|
||||
}}
|
||||
>
|
||||
<div ref={menuContainerRef} style={{ pointerEvents: "auto" }}>
|
||||
{children}
|
||||
</div>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const DropdownMenuItem = styled(MenuItem)`
|
||||
|
||||
`;
|
||||
|
||||
export const DropdownNestedMenuItem = styled(NestedMenuItem)`
|
||||
|
||||
`;
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
export const MythicPageBody = ({children}) => {
|
||||
return (
|
||||
<div style={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
margin: "0.5rem",
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {Typography} from "@mui/material";
|
||||
|
||||
export const MythicPageHeader = ({children, title, style={}, headerVariant="h5"}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Paper elevation={2}
|
||||
variant={"elevation"}
|
||||
style={{
|
||||
backgroundColor: theme.pageHeader.main,
|
||||
color: theme.pageHeaderText.main,
|
||||
border: `1px solid ${theme.borderColor}`,
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: "0.5rem",
|
||||
padding: "0.5rem",
|
||||
borderRadius: "5px",
|
||||
...style
|
||||
}}
|
||||
>
|
||||
<Typography variant={headerVariant} style={{textAlign: "left", display: "inline-block", marginLeft: "1rem", width: "100%"}}>
|
||||
{title}
|
||||
</Typography>
|
||||
<div style={{display: "flex", marginRight: "1rem", alignItems: "center"}}>
|
||||
{children}
|
||||
</div>
|
||||
</Paper>
|
||||
)
|
||||
|
||||
}
|
||||
@@ -1,27 +1,34 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import {classes} from './styles';
|
||||
import Grow from '@mui/material/Grow';
|
||||
import Popper from '@mui/material/Popper';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import MenuList from '@mui/material/MenuList';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import {Dropdown, DropdownMenuItem, DropdownNestedMenuItem} from "../MythicNestedMenus";
|
||||
|
||||
const CellPreMemo = ({ VariableSizeGridProps: { style, rowIndex, columnIndex, data } }) => {
|
||||
const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
const [openContextMenu, setOpenContextMenu] = React.useState(false);
|
||||
const rowClassName = data.gridUUID + "row" + rowIndex;
|
||||
const contextMenuOptions = data?.rowContextMenuOptions || [];
|
||||
const rowHighlight = rowIndex % 2 === 1 ? 'MythicResizableGridRowHighlight' : '';
|
||||
const [contextMenuOptions, setContextMenuOptions] = React.useState(data?.rowContextMenuOptions || []);
|
||||
const dropdownAnchorRef = React.useRef(null);
|
||||
const handleDoubleClick = useCallback(
|
||||
(e) => {
|
||||
data.onDoubleClickRow(e, rowIndex - 1); // minus 1 to account for header row
|
||||
},
|
||||
[data, rowIndex]
|
||||
);
|
||||
const item = data.items[rowIndex][columnIndex];
|
||||
const cellStyle = item?.props?.cellData?.cellStyle || {};
|
||||
const rowStyle = data.items[rowIndex][columnIndex]?.props?.rowData?.rowStyle || {};
|
||||
const contextMenuLocationRef = React.useRef({x: 0, y: 0});
|
||||
const handleDoubleClick = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
data.onDoubleClickRow(e, rowIndex - 1, data.items[rowIndex][columnIndex]?.props?.rowData); // minus 1 to account for header row
|
||||
},
|
||||
[data, rowIndex]
|
||||
);
|
||||
const handleClick = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if(data.onRowClick){
|
||||
data.onRowClick({event, rowDataStatic: data.items[rowIndex][columnIndex]?.props?.rowData})
|
||||
}
|
||||
};
|
||||
const selectedClass = data.items[rowIndex][columnIndex]?.props?.rowData?.selected ? "selectedCallback" : "";
|
||||
const onMouseEnter = () => {
|
||||
const cells = document.getElementsByClassName(rowClassName);
|
||||
if(cells.length > 0){
|
||||
@@ -38,54 +45,120 @@ const CellPreMemo = ({ VariableSizeGridProps: { style, rowIndex, columnIndex, da
|
||||
}
|
||||
}
|
||||
}
|
||||
const handleMenuItemClick = (event, index) => {
|
||||
contextMenuOptions[index].click({event, columnIndex, rowIndex, data: data.items[rowIndex][columnIndex]?.props?.rowData || {}});
|
||||
const handleMenuItemClick = (event, clickOption) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
clickOption({event, columnIndex, rowIndex, data: data.items[rowIndex][columnIndex]?.props?.rowData || {}});
|
||||
setOpenContextMenu(false);
|
||||
};
|
||||
React.useEffect( () => {
|
||||
if(!openContextMenu){
|
||||
onMouseLeave();
|
||||
}
|
||||
}, [openContextMenu]);
|
||||
const handleContextClick = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
if(item.disableFilterMenu){
|
||||
event.stopPropagation();
|
||||
if(item?.disableFilterMenu){
|
||||
return;
|
||||
}
|
||||
if(contextMenuOptions && contextMenuOptions.length > 0){
|
||||
setOpenContextMenu(true);
|
||||
contextMenuLocationRef.current.x = event.clientX;
|
||||
contextMenuLocationRef.current.y = event.clientY;
|
||||
if(data.onRowContextMenuClick){
|
||||
const newMenuItems = data.onRowContextMenuClick({rowDataStatic: data.items[rowIndex][columnIndex]?.props?.rowData});
|
||||
Promise.resolve(newMenuItems).then(function(value) {
|
||||
if(value.length > 0){
|
||||
setContextMenuOptions(value);
|
||||
setOpenContextMenu(true);
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if(contextMenuOptions && contextMenuOptions.length > 0){
|
||||
setOpenContextMenu(true);
|
||||
}
|
||||
}
|
||||
},
|
||||
[contextMenuOptions] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
[contextMenuOptions, data.onRowContextMenuClick] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
);
|
||||
return (
|
||||
<div style={{...style, ...cellStyle, ...rowStyle}}
|
||||
className={`${classes.cell} ${rowClassName}`}
|
||||
className={`${classes.cell} ${rowClassName} ${rowHighlight} ${selectedClass}`}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onContextMenu={handleContextClick}
|
||||
ref={dropdownAnchorRef}
|
||||
>
|
||||
<div className={classes.cellInner}>
|
||||
<div className={classes.cellInner} style={{height: style.height}}>
|
||||
{item}
|
||||
</div>
|
||||
<ContextMenu dropdownAnchorRef={dropdownAnchorRef} contextMenuOptions={contextMenuOptions}
|
||||
disableFilterMenu={item.disableFilterMenu} openContextMenu={openContextMenu}
|
||||
disableFilterMenu={item?.disableFilterMenu} openContextMenu={openContextMenu}
|
||||
contextMenuLocationRef={contextMenuLocationRef}
|
||||
setOpenContextMenu={setOpenContextMenu} handleMenuItemClick={handleMenuItemClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const ContextMenu = ({openContextMenu, dropdownAnchorRef, contextMenuOptions, setOpenContextMenu, handleMenuItemClick}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
export const ContextMenu = ({openContextMenu, dropdownAnchorRef, contextMenuOptions, setOpenContextMenu, handleMenuItemClick, contextMenuLocationRef}) => {
|
||||
const handleClose = (event) => {
|
||||
if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
//if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
|
||||
// return;
|
||||
//}
|
||||
setOpenContextMenu(false);
|
||||
};
|
||||
|
||||
return (
|
||||
openContextMenu &&
|
||||
<Popper open={openContextMenu} anchorEl={dropdownAnchorRef.current} role={undefined} transition disablePortal style={{zIndex: 4}}>
|
||||
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
|
||||
<Dropdown
|
||||
isOpen={dropdownAnchorRef.current}
|
||||
onOpen={setOpenContextMenu}
|
||||
externallyOpen={openContextMenu}
|
||||
absoluteX={contextMenuLocationRef.current.x}
|
||||
absoluteY={contextMenuLocationRef.current.y}
|
||||
anchorReference={"anchorPosition"}
|
||||
menu={[
|
||||
contextMenuOptions.map((option, index) => (
|
||||
option.type === 'item' ? (
|
||||
<DropdownMenuItem
|
||||
key={option.name}
|
||||
disabled={option.disabled}
|
||||
onClick={(event) => handleMenuItemClick(event, option.click)}
|
||||
>
|
||||
{option.icon}{option.name}
|
||||
</DropdownMenuItem>
|
||||
) : option.type === 'menu' ? (
|
||||
<DropdownNestedMenuItem
|
||||
label={option.name}
|
||||
disabled={option.disabled}
|
||||
menu={
|
||||
option.menuItems.map((menuOption, indx) => (
|
||||
<DropdownMenuItem
|
||||
key={menuOption.name}
|
||||
disabled={menuOption.disabled}
|
||||
onClick={(event) => handleMenuItemClick(event, menuOption.click)}
|
||||
>
|
||||
{menuOption.icon}{menuOption.name}
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
}
|
||||
/>
|
||||
) : null
|
||||
)),
|
||||
]}
|
||||
/>
|
||||
</ClickAwayListener>
|
||||
|
||||
)
|
||||
}
|
||||
const Cell = React.memo(CellPreMemo);
|
||||
export default Cell;
|
||||
|
||||
/*
|
||||
<Popper open={openContextMenu} anchorEl={dropdownAnchorRef.current} role={undefined} transition disablePortal style={{zIndex: 4}}>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
@@ -110,8 +183,4 @@ const ContextMenu = ({openContextMenu, dropdownAnchorRef, contextMenuOptions, se
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
)
|
||||
}
|
||||
const Cell = React.memo(CellPreMemo);
|
||||
export default Cell;
|
||||
|
||||
*/
|
||||
+20
-18
@@ -1,9 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import {classes} from './styles';
|
||||
import Draggable from 'react-draggable';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import DragHandleIcon from '@mui/icons-material/DragHandle';
|
||||
|
||||
const DraggableHandles = React.forwardRef(({ height, rowHeight, width, minColumnWidth, columnWidths, onStop }, ref) => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isDragging, setIsDragging] = useState(-1);
|
||||
const nodeRef = React.useRef(null);
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
@@ -11,13 +14,15 @@ const DraggableHandles = React.forwardRef(({ height, rowHeight, width, minColumn
|
||||
style={{
|
||||
height: height,
|
||||
width: width,
|
||||
pointerEvents: isDragging ? 'initial' : 'none',
|
||||
pointerEvents: isDragging >= 0 ? 'initial' : 'none',
|
||||
}}>
|
||||
{columnWidths.map((_, i) => {
|
||||
// leftOffset is the sum of the width of all columns left of i
|
||||
const leftOffset = columnWidths.slice(0, i).reduce((a, b) => a + b, 0);
|
||||
return (
|
||||
<Draggable
|
||||
key={i}
|
||||
nodeRef={nodeRef}
|
||||
axis='x'
|
||||
bounds={{
|
||||
left: minColumnWidth - columnWidths[i],
|
||||
@@ -25,28 +30,25 @@ const DraggableHandles = React.forwardRef(({ height, rowHeight, width, minColumn
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
position={isDragging ? null : { x: 0, y: 0 }}
|
||||
offsetParent={document.body}
|
||||
|
||||
defaultPosition={{x: 0, y: 0}}
|
||||
position={{x: 0, y: 0}}
|
||||
onStart={() => {
|
||||
setIsDragging(i);
|
||||
}}
|
||||
onStop={(e, data) => {
|
||||
setIsDragging(false);
|
||||
setIsDragging(-1);
|
||||
onStop(data.x, i);
|
||||
}}>
|
||||
<div
|
||||
className={classes.draggableHandlesClickArea}
|
||||
style={{
|
||||
left: leftOffset + columnWidths[i] - 1 - 8,
|
||||
height: rowHeight,
|
||||
}}>
|
||||
<div
|
||||
className={classes.draggableHandlesIndicator}
|
||||
<DragHandleIcon
|
||||
ref={nodeRef}
|
||||
className={isDragging === i ? classes.draggableHandlesClickAreaSelected : classes.draggableHandlesClickArea}
|
||||
style={{
|
||||
display: isDragging === i ? 'block' : 'none',
|
||||
height: height / 2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
left: leftOffset + columnWidths[i] - 1 - 7,
|
||||
rotate: isDragging === i ? "0deg" : "90deg",
|
||||
}}>
|
||||
</DragHandleIcon>
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
@@ -54,4 +56,4 @@ const DraggableHandles = React.forwardRef(({ height, rowHeight, width, minColumn
|
||||
);
|
||||
});
|
||||
|
||||
export default DraggableHandles;
|
||||
export default DraggableHandles;
|
||||
@@ -5,13 +5,7 @@ import {classes} from './styles';
|
||||
import React from 'react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faFilter} from '@fortawesome/free-solid-svg-icons';
|
||||
import Grow from '@mui/material/Grow';
|
||||
import Popper from '@mui/material/Popper';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import MenuList from '@mui/material/MenuList';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import {ContextMenu} from "./Cell";
|
||||
|
||||
const HeaderCell = ({
|
||||
onClick = () => {},
|
||||
@@ -23,9 +17,9 @@ const HeaderCell = ({
|
||||
VariableSizeGridProps: { style, rowIndex, columnIndex, data },
|
||||
}) => {
|
||||
const dropdownAnchorRef = React.useRef(null);
|
||||
const theme = useTheme();
|
||||
const item = data.items[rowIndex][columnIndex];
|
||||
const isFiltered = item?.filtered || false;
|
||||
const contextMenuLocationRef = React.useRef({x: 0, y: 0});
|
||||
const handleClick = useCallback(
|
||||
(e) => {
|
||||
onClick(e, columnIndex);
|
||||
@@ -47,22 +41,20 @@ const HeaderCell = ({
|
||||
return;
|
||||
}
|
||||
if(contextMenuOptions && contextMenuOptions.length > 0){
|
||||
|
||||
contextMenuLocationRef.current.x = event.clientX;
|
||||
contextMenuLocationRef.current.y = event.clientY;
|
||||
setOpenContextMenu(true);
|
||||
}
|
||||
},
|
||||
[contextMenuOptions, columnIndex] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
);
|
||||
const handleMenuItemClick = (event, index) => {
|
||||
contextMenuOptions[index].click({event, columnIndex});
|
||||
const handleMenuItemClick = (event, clickOption) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
clickOption({event, columnIndex, rowIndex, data: data.items[rowIndex][columnIndex]?.props?.rowData || {}});
|
||||
setOpenContextMenu(false);
|
||||
};
|
||||
const handleClose = (event) => {
|
||||
if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
setOpenContextMenu(false);
|
||||
};
|
||||
|
||||
const handleClicks = useSingleAndDoubleClick(handleClick, handleDoubleClick);
|
||||
|
||||
return (
|
||||
@@ -73,31 +65,11 @@ const HeaderCell = ({
|
||||
</Typography>
|
||||
{isFiltered && <FontAwesomeIcon icon={faFilter} />}
|
||||
{sortIndicatorIndex === columnIndex && (sortDirection === 'ASC' ? <div>↑</div> : <div>↓</div>)}
|
||||
<Popper open={openContextMenu} anchorEl={dropdownAnchorRef.current} role={undefined} transition disablePortal style={{zIndex: 4}}>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
|
||||
}}
|
||||
>
|
||||
<Paper variant="outlined" style={{backgroundColor: theme.palette.mode === 'dark' ? theme.palette.primary.dark : theme.palette.primary.light, color: "white"}}>
|
||||
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
|
||||
<MenuList id="split-button-menu" >
|
||||
{contextMenuOptions.map((option, index) => (
|
||||
<MenuItem
|
||||
key={option.name + index}
|
||||
onClick={(event) => handleMenuItemClick(event, index)}
|
||||
>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
<ContextMenu dropdownAnchorRef={dropdownAnchorRef} contextMenuOptions={contextMenuOptions}
|
||||
disableFilterMenu={item?.disableFilterMenu} openContextMenu={openContextMenu}
|
||||
contextMenuLocationRef={contextMenuLocationRef}
|
||||
setOpenContextMenu={setOpenContextMenu} handleMenuItemClick={handleMenuItemClick}
|
||||
/>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
|
||||
+137
-44
@@ -7,17 +7,26 @@ import HeaderCell from './HeaderCell';
|
||||
import Cell from './Cell';
|
||||
import DraggableHandles from './DraggableHandles';
|
||||
import {classes} from './styles';
|
||||
import {GetMythicSetting, useSetMythicSetting} from "../MythicSavedUserSetting";
|
||||
|
||||
const HeaderCellContext = createContext({});
|
||||
|
||||
const MIN_COLUMN_WIDTH = 100;
|
||||
const MIN_FLEX_COLUMN_WIDTH = 150;
|
||||
|
||||
const CellRenderer = (VariableSizeGridProps) => {
|
||||
return VariableSizeGridProps.rowIndex === 0 ? null : <Cell VariableSizeGridProps={VariableSizeGridProps} />;
|
||||
|
||||
const CellRendererPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
return rowIndex === 0 ? null : <Cell style={style} rowIndex={rowIndex} columnIndex={columnIndex} data={data} />;
|
||||
};
|
||||
const innerElementType = React.forwardRef(({ children, ...rest }, ref) => {
|
||||
const CellRenderer = React.memo(CellRendererPreMemo);
|
||||
const innerElementType = React.forwardRef(({ children, style }, ref) => {
|
||||
const HeaderCellData = useContext(HeaderCellContext);
|
||||
const onHeaderDoubleClick = React.useCallback( (e, columnIndex) => {
|
||||
if (HeaderCellData.columns[columnIndex].disableAutosize) return;
|
||||
HeaderCellData.autosizeColumn({columnIndex});
|
||||
}, [HeaderCellData.columns, HeaderCellData.autosizeColumn]);
|
||||
return (
|
||||
<div ref={ref} {...rest}>
|
||||
<div ref={ref} style={style}>
|
||||
{/* always render header cells */}
|
||||
<div
|
||||
className={classes.headerCellRow}
|
||||
@@ -31,10 +40,7 @@ const innerElementType = React.forwardRef(({ children, ...rest }, ref) => {
|
||||
key={i}
|
||||
headerNameKey={HeaderCellData.headerNameKey}
|
||||
onClick={HeaderCellData.onClickHeader}
|
||||
onDoubleClick={(e, columnIndex) => {
|
||||
if (column.disableAutosize) return;
|
||||
HeaderCellData.autosizeColumn({columnIndex});
|
||||
}}
|
||||
onDoubleClick={onHeaderDoubleClick}
|
||||
contextMenuOptions={HeaderCellData.contextMenuOptions}
|
||||
sortIndicatorIndex={HeaderCellData.sortIndicatorIndex}
|
||||
sortDirection={HeaderCellData.sortDirection}
|
||||
@@ -60,10 +66,11 @@ const innerElementType = React.forwardRef(({ children, ...rest }, ref) => {
|
||||
);
|
||||
});
|
||||
|
||||
const getShortRandomString = () => {
|
||||
export const GetShortRandomString = () => {
|
||||
return (Math.random() + 1).toString(36).substring(2);
|
||||
}
|
||||
const ResizableGridWrapper = ({
|
||||
name,
|
||||
columns,
|
||||
sortIndicatorIndex,
|
||||
sortDirection,
|
||||
@@ -73,17 +80,31 @@ const ResizableGridWrapper = ({
|
||||
onDoubleClickRow,
|
||||
contextMenuOptions,
|
||||
rowContextMenuOptions,
|
||||
onRowContextMenuClick,
|
||||
rowHeight,
|
||||
headerRowHeight,
|
||||
widthMeasureKey,
|
||||
callbackTableGridRef,
|
||||
onRowClick,
|
||||
...AutoSizerProps
|
||||
}) => {
|
||||
/* Hooks */
|
||||
const { width: scrollbarWidth } = useScrollbarSize();
|
||||
|
||||
const [columnWidths, setColumnWidths] = useState(columns.map((column) => column.width || MIN_COLUMN_WIDTH));
|
||||
const gridUUID = React.useMemo( () => getShortRandomString(), []);
|
||||
const localColumnsRef = React.useRef(columns);
|
||||
const initialSavedWidths = name === undefined ? [] : GetMythicSetting({setting_name: `${name}_column_widths`, default_value: {}, output: "json-array"});
|
||||
const [columnWidths, setColumnWidths] = useState(columns.map((column, index) => {
|
||||
if(initialSavedWidths.length > 0){
|
||||
return initialSavedWidths[index];
|
||||
}
|
||||
if(column.fillWidth){
|
||||
return Math.max(column?.width || 0, MIN_FLEX_COLUMN_WIDTH);
|
||||
}
|
||||
return Math.max(column?.width || 0, MIN_COLUMN_WIDTH);
|
||||
}));
|
||||
const gridUUID = React.useMemo( () => GetShortRandomString(), []);
|
||||
const gridRef = useRef(null);
|
||||
const dragHandlesRef = useRef(null);
|
||||
const [updateSetting] = useSetMythicSetting();
|
||||
const getColumnWidth = useCallback(
|
||||
(index) => {
|
||||
return columnWidths[index] || MIN_COLUMN_WIDTH;
|
||||
@@ -92,16 +113,24 @@ const ResizableGridWrapper = ({
|
||||
);
|
||||
const getRowHeight = useCallback(
|
||||
(index) => {
|
||||
if(index === 0){
|
||||
return headerRowHeight;
|
||||
}
|
||||
return rowHeight;
|
||||
},
|
||||
[rowHeight]
|
||||
[rowHeight, headerRowHeight]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if(initialSavedWidths.length > 0 && localColumnsRef.current.length === columns.length){
|
||||
return;
|
||||
}
|
||||
localColumnsRef.current = columns;
|
||||
const totalWidth = AutoSizerProps.width - scrollbarWidth;
|
||||
const updatedColumnWidths = columns.map((column) => column.width || MIN_COLUMN_WIDTH);
|
||||
const updatedColumnWidths = columns.map((column, index) => {
|
||||
return column.width || MIN_COLUMN_WIDTH
|
||||
});
|
||||
const totalWidthDiff = totalWidth - updatedColumnWidths.reduce((a, b) => a + b, 0);
|
||||
if (totalWidthDiff > 0) {
|
||||
if (totalWidthDiff !== 0) {
|
||||
let updatedWidthIndexs = [];
|
||||
for(let i = 0; i < columns.length; i++){
|
||||
// check if any of the columns have the `fillWidth` property to true
|
||||
@@ -113,46 +142,67 @@ const ResizableGridWrapper = ({
|
||||
updatedWidthIndexs.push(columns.length - 1);
|
||||
}
|
||||
for(let i = 0; i < updatedWidthIndexs.length; i++){
|
||||
updatedColumnWidths[updatedWidthIndexs[i]] += totalWidthDiff / updatedWidthIndexs.length;
|
||||
updatedColumnWidths[updatedWidthIndexs[i]] += Math.max(totalWidthDiff / updatedWidthIndexs.length, MIN_FLEX_COLUMN_WIDTH);
|
||||
}
|
||||
//updatedColumnWidths[updatedWidthIndex] += totalWidthDiff;
|
||||
}
|
||||
setColumnWidths(updatedColumnWidths);
|
||||
}, [scrollbarWidth, columns, AutoSizerProps.width]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
if(name !== undefined){
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
|
||||
}
|
||||
|
||||
}, [scrollbarWidth, columns, AutoSizerProps.width, localColumnsRef.current]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
useEffect(() => {
|
||||
gridRef.current.resetAfterColumnIndex(0, true);
|
||||
if(name !== undefined){
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: columnWidths});
|
||||
}
|
||||
}, [columnWidths]);
|
||||
|
||||
/* Event Handlers */
|
||||
|
||||
const resizeColumn = (x, columnIndex) => {
|
||||
const updatedWidths = columnWidths.map((columnWidth, index) => {
|
||||
const resizeColumn = useCallback( (x, columnIndex) => {
|
||||
const updatedColumnWidths = columnWidths.map((columnWidth, index) => {
|
||||
if (columnIndex === index) {
|
||||
return Math.floor(Math.max(columnWidth + x, MIN_COLUMN_WIDTH));
|
||||
}
|
||||
return Math.floor(columnWidth);
|
||||
});
|
||||
setColumnWidths(updatedWidths);
|
||||
};
|
||||
setColumnWidths(updatedColumnWidths);
|
||||
if(name !== undefined){
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
|
||||
}
|
||||
|
||||
const autosizeColumn = ({columnIndex}) => {
|
||||
}, [columnWidths]);
|
||||
const autosizeColumn = useCallback( ({columnIndex}) => {
|
||||
if(columns[columnIndex].disableDoubleClick){
|
||||
return
|
||||
}
|
||||
const longestElementInColumn = Math.max(...items.map((itemRow) => {
|
||||
if(!columns[columnIndex].key){
|
||||
if(columns[columnIndex].key === undefined){
|
||||
if(columns[columnIndex].plaintext){
|
||||
columns[columnIndex].key = columns[columnIndex].plaintext;
|
||||
} else {
|
||||
return 30;
|
||||
}
|
||||
|
||||
}
|
||||
if(columns[columnIndex].key){
|
||||
if(columns[columnIndex].key !== undefined){
|
||||
if(columns[columnIndex].key.includes("time")){
|
||||
return 30;
|
||||
}
|
||||
if(columns[columnIndex].key === "mythictree_groups"){
|
||||
return itemRow[columnIndex]?.props?.cellData.length;
|
||||
}
|
||||
if(columns[columnIndex].type === "size"){
|
||||
if(itemRow[columnIndex]?.props?.cellData !== undefined){
|
||||
if(typeof itemRow[columnIndex]?.props?.cellData === 'number'){
|
||||
return String(itemRow[columnIndex]?.props?.cellData)?.length;
|
||||
}
|
||||
return String(itemRow[columnIndex]?.props?.cellData?.plaintext)?.length;
|
||||
}
|
||||
return itemRow[columnIndex].length;
|
||||
}
|
||||
try{
|
||||
items = JSON.parse(itemRow[columnIndex]?.props?.rowData?.[columns[columnIndex].key]);
|
||||
if(Array.isArray(items) && items.length > 0){
|
||||
@@ -161,18 +211,26 @@ const ResizableGridWrapper = ({
|
||||
}catch(error){
|
||||
//console.log(itemRow[columnIndex]?.props?.rowData?.[columns[columnIndex].key])
|
||||
}
|
||||
if(typeof itemRow[columnIndex]?.props?.cellData === 'string' ){
|
||||
return itemRow[columnIndex]?.props?.cellData.length;
|
||||
}
|
||||
|
||||
let data = itemRow[columnIndex]?.props?.rowData?.[columns[columnIndex].key];
|
||||
if(!data){
|
||||
return 3;
|
||||
if(columns[columnIndex].inMetadata){
|
||||
return itemRow[columnIndex]?.props?.cellData.length;
|
||||
}
|
||||
if(data === undefined){
|
||||
return MIN_COLUMN_WIDTH;
|
||||
}
|
||||
if(data.plaintext){
|
||||
return String(data.plaintext).length || -1;
|
||||
return String(data.plaintext)?.length;
|
||||
} else if(data?.button?.name) {
|
||||
return String(data?.button?.name)?.length ;
|
||||
} else {
|
||||
return String(data).length || -1;
|
||||
return MIN_COLUMN_WIDTH;
|
||||
}
|
||||
//return String(itemRow[columnIndex]?.props?.rowData?.[columns[columnIndex].key]).length || -1;
|
||||
} else if(typeof(itemRow[columnIndex]?.props?.cellData) === "string") {
|
||||
|
||||
try {
|
||||
items = JSON.parse(itemRow[columnIndex]?.props?.cellData);
|
||||
if (Array.isArray(items) && items.length > 0) {
|
||||
@@ -188,15 +246,33 @@ const ResizableGridWrapper = ({
|
||||
}
|
||||
|
||||
}));
|
||||
const updatedWidths = columnWidths.map((columnWidth, index) => {
|
||||
const updatedColumnWidths = columnWidths.map((columnWidth, index) => {
|
||||
if (columnIndex === index) {
|
||||
if(isNaN(longestElementInColumn)){
|
||||
return MIN_COLUMN_WIDTH;
|
||||
}
|
||||
return Math.floor(Math.max(longestElementInColumn * 10 + 40, MIN_COLUMN_WIDTH));
|
||||
}
|
||||
return Math.floor(columnWidth);
|
||||
});
|
||||
setColumnWidths(updatedWidths);
|
||||
};
|
||||
//console.log(updatedColumnWidths, columnWidths, longestElementInColumn);
|
||||
let updatedValues = false;
|
||||
for(let i = 0; i < updatedColumnWidths.length; i++){
|
||||
if(updatedColumnWidths[i] !== columnWidths[i]){
|
||||
updatedValues = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(updatedValues){
|
||||
setColumnWidths(() => updatedColumnWidths);
|
||||
}
|
||||
}, [columnWidths]);
|
||||
|
||||
useEffect( () => {
|
||||
if(callbackTableGridRef){
|
||||
callbackTableGridRef.current = gridRef.current;
|
||||
}
|
||||
}, [gridRef.current])
|
||||
const itemsWithHeader = [columns, ...items];
|
||||
const headerCellData = {
|
||||
"getRowHeight": getRowHeight,
|
||||
@@ -210,7 +286,13 @@ const ResizableGridWrapper = ({
|
||||
"sortDirection": sortDirection,
|
||||
"getColumnWidth": getColumnWidth,
|
||||
"itemsWithHeader": itemsWithHeader,
|
||||
}
|
||||
};
|
||||
const localOnScroll = React.useCallback( ({scrollLeft}) => {
|
||||
|
||||
if (dragHandlesRef.current) {
|
||||
dragHandlesRef.current.scrollTo({ left: scrollLeft });
|
||||
}
|
||||
}, [dragHandlesRef.current])
|
||||
return (
|
||||
<>
|
||||
<HeaderCellContext.Provider value={headerCellData}>
|
||||
@@ -222,20 +304,20 @@ const ResizableGridWrapper = ({
|
||||
headerNameKey={headerNameKey}
|
||||
rowCount={itemsWithHeader.length}
|
||||
rowHeight={getRowHeight}
|
||||
itemData={{ items: itemsWithHeader, onDoubleClickRow, gridUUID, rowContextMenuOptions}}
|
||||
itemData={{ items: itemsWithHeader,
|
||||
onDoubleClickRow, gridUUID,
|
||||
rowContextMenuOptions, onRowClick,
|
||||
onRowContextMenuClick}}
|
||||
innerElementType={innerElementType}
|
||||
overscanRowCount={5}
|
||||
onScroll={({ scrollLeft }) => {
|
||||
if (dragHandlesRef.current) {
|
||||
dragHandlesRef.current.scrollTo({ left: scrollLeft });
|
||||
}
|
||||
}}
|
||||
overscanRowCount={20}
|
||||
useIsScrolling={false}
|
||||
onScroll={localOnScroll}
|
||||
ref={gridRef}>
|
||||
{CellRenderer}
|
||||
</VariableSizeGrid>
|
||||
<DraggableHandles
|
||||
height={AutoSizerProps.height}
|
||||
rowHeight={getRowHeight(0)}
|
||||
rowHeight={headerRowHeight}
|
||||
width={AutoSizerProps.width}
|
||||
minColumnWidth={MIN_COLUMN_WIDTH}
|
||||
columnWidths={columnWidths}
|
||||
@@ -258,24 +340,34 @@ const MythicResizableGrid = ({
|
||||
onDoubleClickRow,
|
||||
contextMenuOptions,
|
||||
rowContextMenuOptions,
|
||||
onRowContextMenuClick,
|
||||
widthMeasureKey,
|
||||
rowHeight = 32,
|
||||
rowHeight = 20,
|
||||
headerRowHeight = 20,
|
||||
callbackTableGridRef,
|
||||
onRowClick,
|
||||
name,
|
||||
}) => {
|
||||
return (
|
||||
<AutoSizer style={{height: "100%"}}>
|
||||
{(AutoSizerProps) => (
|
||||
<ResizableGridWrapper
|
||||
name={name}
|
||||
columns={columns}
|
||||
callbackTableGridRef={callbackTableGridRef}
|
||||
headerNameKey={headerNameKey}
|
||||
sortIndicatorIndex={sortIndicatorIndex}
|
||||
sortDirection={sortDirection}
|
||||
items={items}
|
||||
widthMeasureKey={widthMeasureKey}
|
||||
rowHeight={rowHeight}
|
||||
headerRowHeight={headerRowHeight}
|
||||
onClickHeader={onClickHeader}
|
||||
onDoubleClickRow={onDoubleClickRow}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
rowContextMenuOptions={rowContextMenuOptions}
|
||||
onRowContextMenuClick={onRowContextMenuClick}
|
||||
onRowClick={onRowClick}
|
||||
{...AutoSizerProps}
|
||||
/>
|
||||
)}
|
||||
@@ -300,6 +392,7 @@ MythicResizableGrid.propTypes = {
|
||||
contextMenuOptions: PropTypes.array,
|
||||
rowContextMenuOptions: PropTypes.array,
|
||||
rowHeight: PropTypes.number,
|
||||
headerRowHeight: PropTypes.number,
|
||||
headerNameKey: PropTypes.string,
|
||||
widthMeasureKey: PropTypes.string
|
||||
};
|
||||
|
||||
@@ -7,5 +7,6 @@ export const classes = {
|
||||
cellInner: `${PREFIX}-cellInner`,
|
||||
draggableHandlesContainer: `${PREFIX}-draggableHandlesContainer`,
|
||||
draggableHandlesClickArea: `${PREFIX}-draggableHandlesClickArea`,
|
||||
draggableHandlesClickAreaSelected: `${PREFIX}-draggableHandlesClickAreaSelected`,
|
||||
draggableHandlesIndicator: `${PREFIX}-draggableHandlesIndicator`,
|
||||
}
|
||||
@@ -1,7 +1,22 @@
|
||||
import { useReactiveVar } from '@apollo/client';
|
||||
import { meState } from '../../cache';
|
||||
import {mePreferences, operatorSettingDefaults} from '../../cache';
|
||||
import React from 'react';
|
||||
import {useMutation, gql } from '@apollo/client';
|
||||
import {snackActions} from "../utilities/Snackbar";
|
||||
|
||||
const updatePreferences = gql`
|
||||
mutation updatePreferences($preferences: jsonb!){
|
||||
updateOperatorPreferences(preferences: $preferences){
|
||||
status
|
||||
error
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const GetComputedFontSize = () => {
|
||||
const element = document.getElementById('root');
|
||||
const fontSizeString = window.getComputedStyle(element).fontSize;
|
||||
return parseFloat(fontSizeString);
|
||||
}
|
||||
/*
|
||||
setting_name options:
|
||||
hideUsernames
|
||||
@@ -9,42 +24,60 @@ setting_name options:
|
||||
showHostname
|
||||
showCallbackGroups
|
||||
showMedia
|
||||
*/
|
||||
export function useMythicSetting({setting_name, default_value, output="boolean"}){
|
||||
const me = useReactiveVar(meState);
|
||||
// get the initial value we have stored
|
||||
interactType
|
||||
|
||||
const localStorageSetting = localStorage.getItem(`${me?.user?.user_id || 0}-${setting_name}`);
|
||||
let initialStorageSetting = localStorageSetting === null ? default_value : localStorageSetting;
|
||||
switch(output){
|
||||
case "boolean":
|
||||
initialStorageSetting = (initialStorageSetting.toLowerCase() === "true");
|
||||
break;
|
||||
case "number":
|
||||
initialStorageSetting = Number(initialStorageSetting);
|
||||
break;
|
||||
default:
|
||||
console.log("unknown output type", output);
|
||||
}
|
||||
callbacks_table_columns
|
||||
callbacks_table_filters
|
||||
*/
|
||||
export function useGetMythicSetting({setting_name, default_value}){
|
||||
const preferences = useReactiveVar(mePreferences);
|
||||
let initialStorageSetting = GetMythicSetting({setting_name, default_value});
|
||||
|
||||
const [setting, setSetting] = React.useState(initialStorageSetting);
|
||||
|
||||
React.useEffect( () => {
|
||||
// update the initial value if the user changes
|
||||
const localStorageSetting = localStorage.getItem(`${me?.user?.user_id || 0}-${setting_name}`);
|
||||
let initialStorageSetting = localStorageSetting === null ? default_value : localStorageSetting;
|
||||
let newSetting = GetMythicSetting({setting_name, default_value});
|
||||
setSetting(newSetting);
|
||||
}, [preferences?.[setting_name]]);
|
||||
|
||||
switch(output){
|
||||
case "boolean":
|
||||
initialStorageSetting = (initialStorageSetting.toLowerCase() === "true");
|
||||
break;
|
||||
case "number":
|
||||
initialStorageSetting = Number(initialStorageSetting);
|
||||
break;
|
||||
default:
|
||||
console.log("unknown output type", output);
|
||||
}
|
||||
|
||||
setSetting(initialStorageSetting);
|
||||
}, [me]);
|
||||
return setting;
|
||||
}
|
||||
export function GetMythicSetting({setting_name, default_value}){
|
||||
const preferences = mePreferences();
|
||||
return preferences?.[setting_name] === undefined ? default_value : preferences?.[setting_name];
|
||||
}
|
||||
export function useSetMythicSetting() {
|
||||
const [updateSetting] = useMutation(updatePreferences, {
|
||||
onCompleted: (data) => {
|
||||
//console.log(data, mePreferences());
|
||||
},
|
||||
onError: (error) => {
|
||||
snackActions.error("failed to save user setting: " + error.message);
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
return [
|
||||
({setting_name, value}) => {
|
||||
if(mePreferences()?.[setting_name] !== value){
|
||||
const updatedPreferences = {
|
||||
...mePreferences(),
|
||||
[setting_name]: value,
|
||||
};
|
||||
mePreferences(updatedPreferences);
|
||||
updateSetting({variables: {preferences: updatedPreferences}});
|
||||
}
|
||||
},
|
||||
({settings}) => {
|
||||
const updatedPreferences = {
|
||||
...mePreferences(),
|
||||
...settings,
|
||||
};
|
||||
mePreferences(updatedPreferences);
|
||||
updateSetting({variables: {preferences: updatedPreferences}});
|
||||
},
|
||||
() => {
|
||||
mePreferences(operatorSettingDefaults);
|
||||
updateSetting({variables: {preferences: operatorSettingDefaults}});
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import MythicStyledTableCell from "./MythicTableCell";
|
||||
|
||||
export function MythicSelectFromListDialog(props) {
|
||||
const [options, setOptions] = React.useState([]);
|
||||
@@ -57,7 +58,7 @@ export function MythicSelectFromListDialog(props) {
|
||||
<em>None</em>
|
||||
</MenuItem>
|
||||
{options.map( (opt) => (
|
||||
<MenuItem value={opt} key={opt[props.identifier]}>{opt[props.display]}</MenuItem>
|
||||
<MenuItem value={opt} key={opt[props.identifier]}>{opt?.[props.display]}</MenuItem>
|
||||
) )}
|
||||
</Select>
|
||||
</FormControl>
|
||||
@@ -89,20 +90,22 @@ export function MythicSelectFromRawListDialog(props) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle >{props.title}</DialogTitle>
|
||||
<TableContainer component={Paper} className="mythicElement">
|
||||
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
<TableBody style={{whiteSpace: "pre"}}>
|
||||
{options.map( choice => (
|
||||
<TableRow hover key={choice}>
|
||||
<TableCell style={{width: "5rem"}}>
|
||||
<Button onClick={() => handleSubmit(choice)} variant="contained" color="primary">Select</Button>
|
||||
</TableCell>
|
||||
<TableCell>{choice}</TableCell>
|
||||
</TableRow>
|
||||
<div style={{height: "100%", display: "flex"}}>
|
||||
<TableContainer component={Paper} className="mythicElement" style={{flexGrow: 1, overflowY: "auto"}}>
|
||||
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
<TableBody style={{whiteSpace: "pre"}}>
|
||||
{options.map( (choice, i) => (
|
||||
<TableRow hover key={choice + i}>
|
||||
<MythicStyledTableCell style={{width: "5rem"}}>
|
||||
<Button onClick={() => handleSubmit(choice)} variant="contained" color="primary">Select</Button>
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>{choice}</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
Close
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Tooltip } from '@mui/material';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
|
||||
export function MythicStyledTooltip(props){
|
||||
const { children, title, style, ...other} = props;
|
||||
export function MythicStyledTooltip({ children, title, enterDelay, tooltipStyle}){
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Tooltip title={title} arrow followCursor enterDelay={1000} componentsProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
backgroundColor: theme.palette.background.contrast,
|
||||
color: theme.palette.text.contrast,
|
||||
boxShadow: theme.shadows[1],
|
||||
fontSize: 13,
|
||||
'& .MuiTooltip-arrow': {
|
||||
color: theme.palette.background.contrast
|
||||
}
|
||||
}
|
||||
}
|
||||
}} style={{zIndex:1000}} {...other}>
|
||||
{<span style={{...style, display: "inline-block"}}>{children}</span>}
|
||||
</Tooltip>
|
||||
);
|
||||
<span style={{display: "inline-block", ...tooltipStyle}}
|
||||
data-tooltip-id={"my-tooltip"}
|
||||
data-tooltip-content={title}
|
||||
data-tooltip-variant={theme.palette.mode === 'dark' ? 'light' : 'dark'}
|
||||
data-tooltip-delay-show={enterDelay ? enterDelay : 750}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
|
||||
)
|
||||
}
|
||||
@@ -2,17 +2,19 @@ import { IconButton } from '@mui/material';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import Grow from '@mui/material/Grow';
|
||||
import Popper from '@mui/material/Popper';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import MenuList from '@mui/material/MenuList';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import Badge from '@mui/material/Badge';
|
||||
import {
|
||||
Draggable,
|
||||
} from "@hello-pangea/dnd";
|
||||
|
||||
export function MythicTabPanel(props) {
|
||||
const { children, value, index, maxHeight, tabInfo, getCallbackData, queryParams, changeSearchParam, ...other } =
|
||||
const { children, value, index, maxHeight, tabInfo, getCallbackData, queryParams, changeSearchParam, showDeleted, ...other } =
|
||||
props;
|
||||
const style =
|
||||
props.style === undefined
|
||||
@@ -37,57 +39,12 @@ export function MythicTabPanel(props) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function a11yProps(index) {
|
||||
export function a11yProps(index) {
|
||||
return {
|
||||
id: `scrollable-auto-tab-${index}`,
|
||||
'aria-controls': `scrollable-auto-tabpanel-${index}`,
|
||||
};
|
||||
}
|
||||
function allowDrop(ev) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
function drag(ev) {
|
||||
ev.dataTransfer.setData("text", ev.target.id);
|
||||
ev.dataTransfer.setData("opacity", ev.target.style.opacity);
|
||||
ev.target.style.opacity = "0.2";
|
||||
}
|
||||
|
||||
function onDragEnter(ev){
|
||||
let node = ev.target.getAttribute("role") === "tab" ? ev.target : ev.target.closest('[role="tab"]');
|
||||
node.style.borderLeft = "3px dotted red";
|
||||
//console.log("ondragenter");
|
||||
}
|
||||
|
||||
function onDragLeave(ev){
|
||||
let node = ev.target.closest('[role="tab"]');
|
||||
if(ev.target.getAttribute("role") === "tab"){
|
||||
//console.log("leaving tab", ev.target)
|
||||
node.style.border = "";
|
||||
} else if(node.contains(ev.target)){
|
||||
//console.log("onDragLeave, nodeContains", node, ev.target)
|
||||
}else{
|
||||
node.style.border = "";
|
||||
//console.log("onDragLeave, not contains", node, ev.target)
|
||||
}
|
||||
}
|
||||
|
||||
function drop(ev) {
|
||||
// shares dataTransfer with drag function
|
||||
ev.preventDefault();
|
||||
const data = ev.dataTransfer.getData("text");
|
||||
const tabList = ev.target.closest("div[role='tablist']");
|
||||
let node = ev.target.getAttribute("role") === "tab" ? ev.target : ev.target.closest('[role="tab"]');
|
||||
//console.log(tabList, data, node.nextSibling);
|
||||
node.style.border = "";
|
||||
document.getElementById(data).style.opacity = ev.dataTransfer.getData("opacity");
|
||||
for(let i = 0; i < tabList.children.length; i++){
|
||||
tabList.children[i].style.border = "";
|
||||
}
|
||||
//tabList.insertBefore(document.getElementById(data), node.nextSibling);
|
||||
//console.log("selected", data, "toLeftOf", node.id);
|
||||
return {"selected": data, "toLeftOf": node.id};
|
||||
}
|
||||
export function MythicTabLabel(props) {
|
||||
const {
|
||||
label,
|
||||
@@ -103,17 +60,21 @@ export function MythicTabLabel(props) {
|
||||
onEditTabDescription,
|
||||
getCallbackData,
|
||||
onDragTab,
|
||||
selectedIndex,
|
||||
highlight,
|
||||
newDataForTab,
|
||||
...other
|
||||
} = props;
|
||||
const onClick = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onCloseTab({ tabID: tabInfo.tabID, index: index });
|
||||
};
|
||||
const theme = useTheme();
|
||||
const [openContextMenu, setOpenContextMenu] = React.useState(false);
|
||||
const dropdownAnchorRef = React.useRef(null);
|
||||
const handleContextClick = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if(contextMenuOptions && contextMenuOptions.length > 0){
|
||||
setOpenContextMenu(true);
|
||||
}
|
||||
@@ -129,51 +90,58 @@ export function MythicTabLabel(props) {
|
||||
setOpenContextMenu(false);
|
||||
};
|
||||
return (
|
||||
<Tab
|
||||
onDrop={(ev) => onDragTab(drop(ev))}
|
||||
onDragOver={allowDrop}
|
||||
onDragEnter={onDragEnter}
|
||||
onDragLeave={onDragLeave}
|
||||
draggable={!!onDragTab}
|
||||
onDragStart={drag}
|
||||
label={
|
||||
<span onContextMenu={handleContextClick} style={{ display: 'inline-block', zIndex: 1}} ref={dropdownAnchorRef}>
|
||||
{label}
|
||||
<IconButton component='div' size='small' onClick={onClick} {...other}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<Draggable draggableId={`callbacks-tab-${tabInfo.tabID}`} index={index} disableInteractiveElementBlocking={true}>
|
||||
{(provided, snapshot) => (
|
||||
<div ref={provided.innerRef}
|
||||
{...provided.draggableProps}>
|
||||
<Tab
|
||||
className={selectedIndex === index || snapshot.isDragging ? "selectedCallback" : "" }
|
||||
label={
|
||||
<span onContextMenu={handleContextClick} style={{}} ref={dropdownAnchorRef}>
|
||||
<Badge color="success" variant="dot" invisible={!highlight} >
|
||||
{label}
|
||||
</Badge>
|
||||
<IconButton component='div' size='small' onClick={onClick} {...other}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</span>
|
||||
}
|
||||
{...a11yProps(index)}
|
||||
{...other}
|
||||
{...provided.dragHandleProps}
|
||||
style={{padding: "0px 5px 0px 5px", borderRadius: "4px", margin: 0,
|
||||
borderBottom: selectedIndex === index ? `2px solid grey` : '',
|
||||
backgroundColor: selectedIndex === index ? tabInfo.color : snapshot.isDragging ? tabInfo.color : ""}}
|
||||
/>
|
||||
<Popper open={openContextMenu} anchorEl={dropdownAnchorRef.current} role={undefined} transition style={{zIndex: 40}}>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
|
||||
}}
|
||||
>
|
||||
<Paper variant="outlined" style={{backgroundColor: theme.palette.mode === 'dark' ? theme.palette.primary.dark : theme.palette.primary.light, color: "white"}}>
|
||||
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
|
||||
<MenuList id="split-button-menu" >
|
||||
{contextMenuOptions.map((option, index) => (
|
||||
<MenuItem
|
||||
key={option.name + index}
|
||||
onClick={(event) => handleMenuItemClick(event, index)}
|
||||
>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
</span>
|
||||
}
|
||||
|
||||
{...a11yProps(index)}
|
||||
{...other}
|
||||
style={{padding: "0px 5px 0px 5px"}}
|
||||
/>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
|
||||
}}
|
||||
>
|
||||
<Paper variant="outlined" className={"dropdownMenuColored"}>
|
||||
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
|
||||
<MenuList id="split-button-menu" >
|
||||
{contextMenuOptions.map((option, index) => (
|
||||
<MenuItem
|
||||
key={option.name + index}
|
||||
onClick={(event) => handleMenuItemClick(event, index)}
|
||||
>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
</div>
|
||||
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
export function MythicSearchTabLabel(props) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {styled} from '@mui/material/styles';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
const MythicStyledTableCell = styled(TableCell, {name: "StyledTableRow", slot: "Wrapper"})({
|
||||
padding: "0 0 0 10px",
|
||||
const MythicStyledTableCell = styled(TableCell, {})({
|
||||
padding: "2px 0 2px 10px !important",
|
||||
});
|
||||
export default MythicStyledTableCell;
|
||||
@@ -5,7 +5,7 @@ import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import MythicTextField from './MythicTextField';
|
||||
import {useQuery, gql, useMutation} from '@apollo/client';
|
||||
import { Select, Input, MenuItem, Link, IconButton, } from '@mui/material';
|
||||
import { Select, Input, MenuItem, Link, IconButton } from '@mui/material';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
@@ -24,14 +24,22 @@ import WebhookIcon from '@mui/icons-material/Webhook';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import LocalOfferOutlinedIcon from '@mui/icons-material/LocalOfferOutlined';
|
||||
import {MythicStyledTooltip} from "./MythicStyledTooltip";
|
||||
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import MythicStyledTableCell from "./MythicTableCell";
|
||||
import {meState} from "../../cache";
|
||||
import { useReactiveVar } from '@apollo/client';
|
||||
import {MythicDraggableDialogTitle} from "./MythicDraggableDialogTitle";
|
||||
|
||||
const createNewTagMutationTemplate = ({target_object}) => {
|
||||
// target_object should be something like "task_id"
|
||||
// target_object_id should be something like "89"
|
||||
return gql`
|
||||
mutation createNewTag($url: String!, $data: jsonb!, $source: String!, $${target_object}: Int!, $tagtype_id: Int!){
|
||||
insert_tag_one(object: {url: $url, data: $data, source: $source, ${target_object}: $${target_object}, tagtype_id: $tagtype_id}){
|
||||
createTag(url: $url, data: $data, source: $source, ${target_object}: $${target_object}, tagtype_id: $tagtype_id){
|
||||
id
|
||||
status
|
||||
error
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -46,7 +54,7 @@ const updateTagMutationTemplate = gql`
|
||||
const getObjectTagsQueryTemplate = ({target_object}) => {
|
||||
return gql`
|
||||
query getObjectTags ($${target_object}: Int!) {
|
||||
tag(where: {${target_object}: {_eq: $${target_object}}}) {
|
||||
tag(where: {${target_object}: {_eq: $${target_object}}}, order_by: {tagtype: {name: asc}}) {
|
||||
source
|
||||
url
|
||||
id
|
||||
@@ -63,7 +71,7 @@ query getObjectTags ($${target_object}: Int!) {
|
||||
}
|
||||
const getTagtypesQuery = gql`
|
||||
query getTagtype {
|
||||
tagtype {
|
||||
tagtype(order_by: {name: asc}) {
|
||||
name
|
||||
color
|
||||
description
|
||||
@@ -85,6 +93,17 @@ query getSingleTag($tag_id: Int!){
|
||||
url
|
||||
id
|
||||
data
|
||||
apitokens_id
|
||||
credential_id
|
||||
filemeta_id
|
||||
keylog_id
|
||||
mythictree_id
|
||||
operation_id
|
||||
response_id
|
||||
callback_id
|
||||
payload_id
|
||||
task_id
|
||||
taskartifact_id
|
||||
tagtype {
|
||||
name
|
||||
description
|
||||
@@ -94,15 +113,16 @@ query getSingleTag($tag_id: Int!){
|
||||
}
|
||||
}
|
||||
`
|
||||
export const TagsDisplay = ({tags}) => {
|
||||
export const TagsDisplay = ({tags, expand}) => {
|
||||
return (
|
||||
tags?.map( tt => (
|
||||
<TagChipDisplay tag={tt} key={tt.id} />
|
||||
<TagChipDisplay tag={tt} key={tt.id} expand={expand} />
|
||||
))
|
||||
)
|
||||
}
|
||||
const TagChipDisplay = ({tag}) => {
|
||||
const TagChipDisplay = ({tag, expand}) => {
|
||||
const [openTagDisplay, setOpenTagDisplay] = React.useState(false);
|
||||
const [label, setLabel] = React.useState(expand ? tag.tagtype.name : tag.tagtype.name[0]);
|
||||
const onSelectTag = (event, tag) => {
|
||||
if(event){
|
||||
event.preventDefault();
|
||||
@@ -117,9 +137,26 @@ const TagChipDisplay = ({tag}) => {
|
||||
}
|
||||
setOpenTagDisplay(false);
|
||||
}
|
||||
const onMouseOver = () => {
|
||||
if(expand === undefined || !expand){
|
||||
setLabel(tag.tagtype.name);
|
||||
}
|
||||
}
|
||||
const onMouseOut = () => {
|
||||
if(expand === undefined || !expand){
|
||||
setTimeout( () => {
|
||||
setLabel(tag.tagtype.name[0]);
|
||||
}, 10000); // wait 10s then go back to just a single letter
|
||||
}
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Chip label={tag.tagtype.name} size="small" onClick={(e) => onSelectTag(e)} style={{float: "right", backgroundColor:tag.tagtype.color}} />
|
||||
<Chip onMouseOver={onMouseOver} onMouseOut={onMouseOut} label={label} size="small" onClick={(e) => onSelectTag(e)}
|
||||
style={{float: "right", backgroundColor:tag.tagtype.color, height: "15px"}}
|
||||
sx={{
|
||||
"& .MuiChip-label": {overflow: "visible"}
|
||||
}}
|
||||
/>
|
||||
{openTagDisplay &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openTagDisplay}
|
||||
onClose={onClose}
|
||||
@@ -151,7 +188,7 @@ const StringTagDataEntry = ({name, value}) => {
|
||||
console.log("There was an error!", error);
|
||||
})
|
||||
}
|
||||
if(RegExp(regex).test(value)){
|
||||
if(RegExp(regex)?.test(value)){
|
||||
const capturePieces = RegExp(captureRegex).exec(value);
|
||||
const targetPieces = RegExp(targetRegex).exec(capturePieces[3]);
|
||||
const colorPieces = RegExp(colorRegex).exec(capturePieces[3]);
|
||||
@@ -173,7 +210,7 @@ const StringTagDataEntry = ({name, value}) => {
|
||||
return (
|
||||
<Link href={capturePieces[2]} color="textPrimary" target={"_blank"} >{capturePieces[1]}</Link>
|
||||
)
|
||||
} else if(value.startsWith("http")){
|
||||
} else if(value.startsWith("http:") || value.startsWith("https:")){
|
||||
return (
|
||||
<>
|
||||
{"Click for: "}
|
||||
@@ -186,22 +223,50 @@ const StringTagDataEntry = ({name, value}) => {
|
||||
function ViewTagDialog(props) {
|
||||
const theme = useTheme();
|
||||
const [selectedTag, setSelectedTag] = React.useState({});
|
||||
const [objectInfo, setObjectInfo] = React.useState({object_type: "", object_id: ""});
|
||||
const {} = useQuery(getSingleTag, {
|
||||
variables: {tag_id: props.target_object_id},
|
||||
onCompleted: data => {
|
||||
if(data.tag_by_pk.apitokens_id !== null){
|
||||
setObjectInfo({object_type: "apitokens_id", object_id: data.tag_by_pk.apitokens_id});
|
||||
}else if(data.tag_by_pk.credential_id !== null){
|
||||
setObjectInfo({object_type: "credential_id", object_id: data.tag_by_pk.credential_id});
|
||||
}else if(data.tag_by_pk.filemeta_id !== null){
|
||||
setObjectInfo({object_type: "filemeta_id", object_id: data.tag_by_pk.filemeta_id});
|
||||
}else if(data.tag_by_pk.keylog_id !== null){
|
||||
setObjectInfo({object_type: "keylog_id", object_id: data.tag_by_pk.keylog_id});
|
||||
}else if(data.tag_by_pk.mythictree_id !== null){
|
||||
setObjectInfo({object_type: "mythictree_id", object_id: data.tag_by_pk.mythictree_id});
|
||||
}else if(data.tag_by_pk.response_id !== null){
|
||||
setObjectInfo({object_type: "response_id", object_id: data.tag_by_pk.response_id});
|
||||
}else if(data.tag_by_pk.task_id !== null){
|
||||
setObjectInfo({object_type: "task_id", object_id: data.tag_by_pk.task_id});
|
||||
}else if(data.tag_by_pk.taskartifact_id !== null){
|
||||
setObjectInfo({object_type: "taskartifact_id", object_id: data.tag_by_pk.taskartifact_id});
|
||||
}else if(data.tag_by_pk.payload_id !== null){
|
||||
setObjectInfo({object_type: "payload_id", object_id: data.tag_by_pk.payload_id});
|
||||
}else if(data.tag_by_pk.callback_id !== null){
|
||||
setObjectInfo({object_type: "callback_id", object_id: data.tag_by_pk.callback_id});
|
||||
}
|
||||
let newTag = {...data.tag_by_pk};
|
||||
let tagData = newTag;
|
||||
try{
|
||||
tagData = JSON.parse(newTag.data);
|
||||
newTag.data = tagData;
|
||||
newTag.is_json = true;
|
||||
if(newTag.data.constructor === Object){
|
||||
newTag.data = {...data.tag_by_pk.data};
|
||||
newTag.is_json = true;
|
||||
} else if(typeof newTag.data === "string"){
|
||||
tagData = JSON.parse(newTag.data);
|
||||
newTag.data = tagData;
|
||||
newTag.is_json = true;
|
||||
}
|
||||
|
||||
}catch(error){
|
||||
newTag.is_json = false;
|
||||
}
|
||||
setSelectedTag(newTag);
|
||||
},
|
||||
onError: error => {
|
||||
console.log(error);
|
||||
console.log("query error", error);
|
||||
},
|
||||
fetchPolicy: "network-only"
|
||||
})
|
||||
@@ -212,17 +277,21 @@ function ViewTagDialog(props) {
|
||||
}
|
||||
props.onClose(event);
|
||||
}
|
||||
|
||||
const stopClicks = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title" onClick={(e) => e.stopPropagation()}>View Tag</DialogTitle>
|
||||
<TableContainer className="mythicElement">
|
||||
<DialogTitle id="form-dialog-title" onClick={stopClicks}>View Tag</DialogTitle>
|
||||
<TableContainer className="mythicElement" onClick={stopClicks}>
|
||||
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
<TableBody>
|
||||
<TableRow hover>
|
||||
<TableCell style={{width: "20%"}}>Tag Type</TableCell>
|
||||
<TableCell style={{display: "inline-flex", flexDirection: "row", width: "100%"}}>
|
||||
<Chip label={selectedTag?.tagtype?.name||""} size="small" style={{float: "right", backgroundColor:selectedTag?.tagtype?.color||""}} />
|
||||
<ViewEditTags target_object={objectInfo.object_type} target_object_id={objectInfo.object_id} me={props.me} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
@@ -230,38 +299,60 @@ return (
|
||||
<TableCell>{selectedTag?.tagtype?.description || ""}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>Source</TableCell>
|
||||
<TableCell>
|
||||
<MythicStyledTableCell>Source</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
{selectedTag?.source ||""}
|
||||
</TableCell>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>Reference URL</TableCell>
|
||||
<TableCell>
|
||||
<Link href={selectedTag?.url || "#"} color="textPrimary" target="_blank" referrerPolicy='no'>{selectedTag?.url ? "click here" : "No reference link provided"}</Link>
|
||||
</TableCell>
|
||||
<MythicStyledTableCell>Reference URL</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
{selectedTag?.url === "" ? (
|
||||
"No reference link provided"
|
||||
) : (
|
||||
<Link href={selectedTag?.url || "#"} color="textPrimary" target="_blank" referrerPolicy='no'>{selectedTag?.url ? "click here" : "No reference link provided"}</Link>
|
||||
)}
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Data</TableCell>
|
||||
<TableCell>
|
||||
{selectedTag?.is_json || false ? (
|
||||
{selectedTag?.is_json ? (
|
||||
<TableContainer className="mythicElement">
|
||||
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
<TableBody>
|
||||
{Object.keys(selectedTag.data).map( key => (
|
||||
<TableRow key={key} hover>
|
||||
<TableCell>{key}</TableCell>
|
||||
<MythicStyledTableCell>{key}</MythicStyledTableCell>
|
||||
{typeof selectedTag.data[key] === "string" ? (
|
||||
<TableCell>
|
||||
<StringTagDataEntry name={key} value={selectedTag.data[key]} />
|
||||
</TableCell>
|
||||
<MythicStyledTableCell style={{whiteSpace: "pre-wrap"}}>
|
||||
<StringTagDataEntry name={key} value={String(selectedTag.data[key])} />
|
||||
</MythicStyledTableCell>
|
||||
) : typeof selectedTag.data[key] === "object" ? (
|
||||
<TableCell>{selectedTag.data[key].toString()}</TableCell>
|
||||
Array.isArray(selectedTag.data[key]) ? (
|
||||
<TableCell style={{whiteSpace: "pre-wrap"}}>{JSON.stringify(selectedTag.data[key], null, 2)}</TableCell>
|
||||
) : (
|
||||
<MythicStyledTableCell>
|
||||
<Table size={"small"} >
|
||||
<TableBody>
|
||||
{Object.keys(selectedTag.data[key]).map(key2 => (
|
||||
<TableRow key={key2} >
|
||||
<MythicStyledTableCell>{key2}</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<StringTagDataEntry name={key2} value={String(selectedTag.data[key][key2])} />
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
</Table>
|
||||
</MythicStyledTableCell>
|
||||
)
|
||||
) : typeof selectedTag.data[key] === "boolean" ? (
|
||||
<TableCell>{selectedTag.data[key] ? "True" : "False"}</TableCell>
|
||||
<MythicStyledTableCell>{selectedTag.data[key] ? "True" : "False"}</MythicStyledTableCell>
|
||||
) :
|
||||
(
|
||||
<TableCell>{selectedTag.data[key]}</TableCell>
|
||||
<MythicStyledTableCell>{String(selectedTag.data[key])}</MythicStyledTableCell>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -318,7 +409,16 @@ export function ViewEditTagsDialog(props) {
|
||||
setSelectedTag(data.tag[0]);
|
||||
setNewSource(data.tag[0].source);
|
||||
setNewURL(data.tag[0].url);
|
||||
setNewData(data.tag[0].data);
|
||||
try{
|
||||
if(typeof data.tag[0].data !== "string"){
|
||||
setNewData(JSON.stringify(data.tag[0].data, null, 2));
|
||||
} else {
|
||||
setNewData(String(data.tag[0].data));
|
||||
}
|
||||
}catch(error){
|
||||
setNewData(String(data.tag[0].data));
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
onError: error => {
|
||||
@@ -333,7 +433,11 @@ export function ViewEditTagsDialog(props) {
|
||||
setExistingTags(newTags);
|
||||
if(newTags.length > 0){
|
||||
setSelectedTag(newTags[0]);
|
||||
setNewData(newTags[0].data);
|
||||
try{
|
||||
setNewData(JSON.stringify(newTags[0].data, null, 2));
|
||||
}catch(error){
|
||||
setNewData(String(newTags[0].data));
|
||||
}
|
||||
setNewSource(newTags[0].source);
|
||||
setNewURL(newTags[0].url);
|
||||
} else {
|
||||
@@ -374,9 +478,13 @@ const onEditorChange = (value, event) => {
|
||||
}
|
||||
const handleTaskTypeChange = (evt) => {
|
||||
setSelectedTag(evt.target.value);
|
||||
setNewData(evt.target.value.data);
|
||||
setNewSource(evt.target.value.source);
|
||||
setNewURL(evt.target.value.url);
|
||||
try{
|
||||
setNewData(JSON.stringify(evt.target.value.data, null, 2));
|
||||
}catch(error){
|
||||
setNewData(String(evt.target.value.data));
|
||||
}
|
||||
}
|
||||
const handleNewTagCreate = ({tagtype_id, source, url, data, id}) => {
|
||||
props.onClose();
|
||||
@@ -388,25 +496,26 @@ const onAcceptDelete = () => {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Edit Tags
|
||||
<Button variant='contained' color="success" style={{float: "right"}} onClick={() => setOpenNewDialog(true)} >New</Button>
|
||||
</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
{openNewDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="xl" open={openNewDialog}
|
||||
<MythicDraggableDialogTitle >Edit Tags </MythicDraggableDialogTitle>
|
||||
<DialogContent dividers={true} style={{width: "100%"}}>
|
||||
{openNewDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openNewDialog}
|
||||
onClose={()=>{setOpenNewDialog(false);}}
|
||||
innerDialog={<NewTagDialog me={props.me}
|
||||
target_object={props.target_object}
|
||||
target_object_id={props.target_object_id}
|
||||
onClose={()=>{setOpenNewDialog(false);}}
|
||||
onSubmit={handleNewTagCreate} />}
|
||||
/>) : null}
|
||||
<TableContainer className="mythicElement">
|
||||
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
/>}
|
||||
<Table size="small" style={{ width: "100%", overflow: "scroll"}}>
|
||||
<TableBody>
|
||||
<TableRow hover>
|
||||
<TableCell style={{width: "30%"}}>Select Existing Tag to Edit</TableCell>
|
||||
<TableCell style={{display: "inline-flex", flexDirection: "row-reverse"}}>
|
||||
<MythicStyledTableCell style={{width: "30%"}}>Select Existing Tag to Edit or Add New</MythicStyledTableCell>
|
||||
<MythicStyledTableCell style={{display: "inline-flex", flexDirection: "row-reverse"}}>
|
||||
<MythicStyledTooltip title={"Add New Tag"}>
|
||||
<IconButton variant='contained' color="success" style={{float: "right"}} onClick={() => setOpenNewDialog(true)} >
|
||||
<AddCircleOutlineIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<Select
|
||||
labelId="demo-dialog-select-label"
|
||||
id="demo-dialog-select"
|
||||
@@ -423,32 +532,31 @@ return (
|
||||
{selectedTag.id &&
|
||||
<IconButton size="small" style={{float: "right"}} onClick={()=>{setOpenDeleteDialog(true);}} color="error" variant="contained"><DeleteIcon/></IconButton>
|
||||
}
|
||||
|
||||
{openDelete &&
|
||||
<MythicConfirmDialog onClose={() => {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete} open={openDelete}/>
|
||||
}
|
||||
</TableCell>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>Tag Description</TableCell>
|
||||
<TableCell>{selectedTag?.tagtype?.description || ""}</TableCell>
|
||||
<MythicStyledTableCell>Tag Description</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>{selectedTag?.tagtype?.description || ""}</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>Source</TableCell>
|
||||
<TableCell>
|
||||
<MythicStyledTableCell>Source</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<MythicTextField value={newSource} onChange={onChangeSource} name="Source of tag data" />
|
||||
</TableCell>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>External URL</TableCell>
|
||||
<TableCell>
|
||||
<MythicStyledTableCell>External URL</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<MythicTextField value={newURL} onChange={onChangeURL} name="External URL reference" />
|
||||
<Link href={newURL} color="textPrimary" target="_blank" referrerPolicy='no'>{newURL ? "click here" : ""}</Link>
|
||||
</TableCell>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>JSON Data</TableCell>
|
||||
<TableCell>
|
||||
<MythicStyledTableCell>JSON Data</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
|
||||
@@ -464,11 +572,10 @@ return (
|
||||
tabSize: 4,
|
||||
useWorker: false
|
||||
}}/>
|
||||
</TableCell>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
@@ -502,9 +609,14 @@ export function NewTagDialog(props) {
|
||||
});
|
||||
const [newTag] = useMutation(createNewTagMutationTemplate({target_object: props.target_object}), {
|
||||
onCompleted: data => {
|
||||
snackActions.success("Successfully created new tag!");
|
||||
props.onSubmit({source:newSource, url:newURL, data:newData, tagtype_id:selectedTagType.id, id: data.insert_tag_one.id});
|
||||
props.onClose()
|
||||
if(data.createTag.status === "success"){
|
||||
snackActions.success("Successfully created new tag!");
|
||||
props.onSubmit({source:newSource, url:newURL, data:newData, tagtype_id:selectedTagType.id, id: data.createTag.id});
|
||||
props.onClose()
|
||||
} else {
|
||||
snackActions.error(data.createTag.error);
|
||||
}
|
||||
|
||||
},
|
||||
onError: error => {
|
||||
snackActions.error(error.message);
|
||||
@@ -532,14 +644,25 @@ export function NewTagDialog(props) {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Create new Tag Instance</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
<TableContainer className="mythicElement">
|
||||
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
<MythicDraggableDialogTitle >Add New Tag</MythicDraggableDialogTitle>
|
||||
<DialogContent dividers={true} style={{width: "100%"}}>
|
||||
<Table size="small" style={{ overflow: "scroll", width: "100%"}}>
|
||||
<TableBody>
|
||||
<TableRow hover>
|
||||
<TableCell style={{width: "30%"}}>Select Existing Tag Type</TableCell>
|
||||
<TableCell>
|
||||
<MythicStyledTableCell style={{width: "20%"}}>
|
||||
<Typography>
|
||||
Tag
|
||||
</Typography>
|
||||
<Typography size="small" component="span" style={{fontSize: theme.typography.pxToRem(15)}}>
|
||||
To create a new tag type click <Link style={{wordBreak: "break-all"}}
|
||||
color="textPrimary"
|
||||
href={"/new/tagtypes"}
|
||||
underline="always" target="_blank">
|
||||
here
|
||||
</Link>
|
||||
</Typography>
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<Select
|
||||
labelId="demo-dialog-select-label"
|
||||
id="demo-dialog-select"
|
||||
@@ -553,24 +676,24 @@ export function NewTagDialog(props) {
|
||||
</MenuItem>
|
||||
) )}
|
||||
</Select>
|
||||
</TableCell>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>Source</TableCell>
|
||||
<TableCell>
|
||||
<MythicStyledTableCell>Source</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<MythicTextField value={newSource} onChange={onChangeSource} name="Source of tag data" />
|
||||
</TableCell>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>External URL</TableCell>
|
||||
<TableCell>
|
||||
<MythicStyledTableCell>External URL</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<MythicTextField value={newURL} onChange={onChangeURL} name="External URL reference" />
|
||||
<Link href={newURL} color="textPrimary" target="_blank" referrerPolicy='no'>{newURL}</Link>
|
||||
</TableCell>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>JSON Data</TableCell>
|
||||
<TableCell>
|
||||
<MythicStyledTableCell>JSON Data</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
|
||||
@@ -586,11 +709,10 @@ export function NewTagDialog(props) {
|
||||
tabSize: 4,
|
||||
useWorker: false
|
||||
}}/>
|
||||
</TableCell>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
@@ -606,7 +728,8 @@ export function NewTagDialog(props) {
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
export const ViewEditTags = ({target_object, target_object_id, me}) => {
|
||||
export const ViewEditTags = ({target_object, target_object_id}) => {
|
||||
const me = useReactiveVar(meState);
|
||||
const [openTagDialog, setOpenTagDialog] = React.useState(false);
|
||||
const toggleTagDialog = (event, open) => {
|
||||
if(event){
|
||||
@@ -617,12 +740,15 @@ export const ViewEditTags = ({target_object, target_object_id, me}) => {
|
||||
}
|
||||
return(
|
||||
<React.Fragment>
|
||||
<IconButton onClick={(e) => toggleTagDialog(e, true)} size="small" style={{display: "inline-block", float: "right"}}><LocalOfferOutlinedIcon /></IconButton>
|
||||
{openTagDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="xl" open={openTagDialog}
|
||||
<IconButton onClick={(e) => toggleTagDialog(e, true)} size="small"
|
||||
style={{display: "inline-block", float: "right", padding: "0px"}}>
|
||||
<LocalOfferOutlinedIcon />
|
||||
</IconButton>
|
||||
{openTagDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openTagDialog}
|
||||
onClose={(e)=>{toggleTagDialog(e, false)}}
|
||||
innerDialog={<ViewEditTagsDialog me={me} target_object={target_object} target_object_id={target_object_id} onClose={(e)=>{toggleTagDialog(e, false)}} />}
|
||||
/>) : null}
|
||||
/>}
|
||||
</React.Fragment>
|
||||
)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export function MythicTextEditDialog(props) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">{props.title}</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
<DialogContent dividers={true} style={{padding: 0}}>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
|
||||
@@ -24,6 +24,8 @@ export function MythicTextEditDialog(props) {
|
||||
showGutter={true}
|
||||
highlightActiveLine={true}
|
||||
value={props.value}
|
||||
showPrintMargin={false}
|
||||
wrapEnabled={true}
|
||||
focus={true}
|
||||
width={"100%"}
|
||||
setOptions={{
|
||||
|
||||
@@ -1,108 +1,133 @@
|
||||
import React from 'react';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import PropTypes from 'prop-types';
|
||||
import {TextField} from '@mui/material';
|
||||
import {useDebounce} from "../utilities/useDebounce";
|
||||
const PREFIX = 'MythicTextField';
|
||||
|
||||
const classes = {
|
||||
root: `${PREFIX}-root`
|
||||
root: `${PREFIX}Div-root`,
|
||||
textFieldRoot: `${PREFIX}-root`
|
||||
};
|
||||
|
||||
const Root = styled('div')({
|
||||
const Root = styled('div')(({theme}) => ({
|
||||
[`&.${classes.root}`]: {
|
||||
'& input:valid + fieldset': {
|
||||
borderColor: 'grey',
|
||||
borderWidth: 1,
|
||||
|
||||
},
|
||||
}));
|
||||
|
||||
const ValidationTextField = styled(TextField)(({theme}) => ({
|
||||
[`&.${classes.textFieldRoot}`]: {
|
||||
'& fieldset': {
|
||||
borderColor: 'grey',
|
||||
borderWidth: 1,
|
||||
},
|
||||
'& input:invalid + fieldset': {
|
||||
borderColor: 'red',
|
||||
borderWidth: 2,
|
||||
borderColor: 'red',
|
||||
borderWidth: 2,
|
||||
},
|
||||
'& input:valid:focus + fieldset': {
|
||||
borderLeftWidth: 6,
|
||||
padding: '4px !important', // override inline-style
|
||||
borderLeftWidth: 6,
|
||||
padding: '4px !important', // override inline-style
|
||||
},
|
||||
},
|
||||
});
|
||||
'& textarea:focus + textarea + fieldset': {
|
||||
borderLeftWidth: 6,
|
||||
//padding: '4px !important', // override inline-style
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const ValidationTextField = TextField;
|
||||
|
||||
class MythicTextField extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
placeholder: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
validate: PropTypes.func,
|
||||
width: PropTypes.number,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
requiredValue: PropTypes.bool,
|
||||
type: PropTypes.string,
|
||||
onEnter: PropTypes.func,
|
||||
autoFocus: PropTypes.bool,
|
||||
autoComplete: PropTypes.bool,
|
||||
showLabel: PropTypes.bool,
|
||||
variant: PropTypes.string,
|
||||
inline: PropTypes.bool,
|
||||
marginBottom: PropTypes.string,
|
||||
}
|
||||
onChange = evt => {
|
||||
const name = this.props.name;
|
||||
const value = evt.target.value;
|
||||
const error = this.props.validate ? this.props.validate(value) : false;
|
||||
this.props.onChange(name, value, error, evt);
|
||||
}
|
||||
checkError = () => {
|
||||
return this.props.validate ? this.props.validate(this.props.value) : false
|
||||
}
|
||||
onKeyPress = (event) => {
|
||||
const MythicTextField = ({
|
||||
placeholder,
|
||||
name,
|
||||
validate,
|
||||
width,
|
||||
onChange,
|
||||
requiredValue=false,
|
||||
type = "text",
|
||||
onEnter,
|
||||
autoFocus,
|
||||
autoComplete=false,
|
||||
showLabel = true,
|
||||
variant = "outlined",
|
||||
inline,
|
||||
marginBottom = "5px",
|
||||
value,
|
||||
disabled = false,
|
||||
marginTop = "5px",
|
||||
InputProps = {},
|
||||
inputLabelProps = {},
|
||||
multiline = false,
|
||||
maxRows = 10,
|
||||
errorText = "",
|
||||
helperText = "",
|
||||
debounceDelay = 100,
|
||||
}) => {
|
||||
const [localValue, setLocalValue] = React.useState({value: value, event: null});
|
||||
const [localError, setLocalError] = React.useState(false);
|
||||
const debouncedLocalInput = useDebounce(localValue, debounceDelay);
|
||||
React.useEffect( () => {
|
||||
const error = validate ? validate(debouncedLocalInput.value) : false;
|
||||
onChange(name, debouncedLocalInput.value, error, debouncedLocalInput.event);
|
||||
}, [debouncedLocalInput]);
|
||||
React.useEffect( () => {
|
||||
setLocalValue({value: value, event: null});
|
||||
}, [value]);
|
||||
const handleChange = (evt) => {
|
||||
const newValue = evt.target.value;
|
||||
// Update local state immediately for responsive UI
|
||||
setLocalValue({value: newValue, event: evt});
|
||||
setLocalError(validate ? validate(newValue) : false);
|
||||
};
|
||||
const onKeyPress = (event) => {
|
||||
if(event.key === "Enter") {
|
||||
if(event.shiftKey){
|
||||
this.onChange(event);
|
||||
handleChange(event);
|
||||
return;
|
||||
}
|
||||
if (this.props.onEnter !== undefined) {
|
||||
if (onEnter !== undefined) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.props.onEnter(event);
|
||||
onEnter(event);
|
||||
}
|
||||
}else{
|
||||
this.onChange(event);
|
||||
handleChange(event);
|
||||
}
|
||||
}
|
||||
render(){
|
||||
return (
|
||||
<Root style={{width: this.props.width ? this.props.width + "rem" : "100%", display: this.props.inline ? "inline-block": "",}}>
|
||||
<ValidationTextField
|
||||
fullWidth={true}
|
||||
placeholder={this.props.placeholder}
|
||||
value={this.props.value}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyPress}
|
||||
label={this.props.showLabel === undefined ? this.props.name : this.props.showLabel ? this.props.name : undefined}
|
||||
autoFocus={this.props.autoFocus}
|
||||
variant={this.props.variant === undefined ? "outlined" : this.props.variant}
|
||||
data-lpignore={true}
|
||||
autoComplete={this.props.autoComplete === undefined ? "off" : (this.props.autoComplete ? "on" : "off")}
|
||||
disabled={this.props.disabled === undefined ? false : this.props.disabled}
|
||||
required={this.props.requiredValue ? this.props.requiredValue : false}
|
||||
InputLabelProps={this.props.inputLabelProps}
|
||||
multiline={this.props.multiline ? this.props.multiline : false}
|
||||
maxRows={this.props.maxRows}
|
||||
error={this.checkError()}
|
||||
type={this.props.type === undefined ? "text" : this.props.type}
|
||||
onWheel={ event => event.target.blur() }
|
||||
InputProps={this.props.InputProps}
|
||||
helperText={this.checkError() ? this.props.errorText : this.props.helperText}
|
||||
style={{
|
||||
padding:0,
|
||||
marginBottom: this.props.marginBottom ? this.props.marginBottom : "10px",
|
||||
display: this.props.inline ? "inline-block": "",
|
||||
}}
|
||||
classes={{
|
||||
root: classes.root
|
||||
}} />
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Root style={{width: width ? width + "rem" : "100%", display: inline ? "inline-block": "",}}>
|
||||
<ValidationTextField
|
||||
fullWidth={true}
|
||||
placeholder={placeholder}
|
||||
value={localValue.value}
|
||||
onChange={handleChange}
|
||||
color={"secondary"}
|
||||
onKeyDown={onKeyPress}
|
||||
label={showLabel ? name : undefined}
|
||||
autoFocus={autoFocus}
|
||||
variant={variant}
|
||||
data-lpignore={true}
|
||||
autoComplete={autoComplete === undefined ? "new-password" : (autoComplete ? "on" : "off")}
|
||||
disabled={disabled}
|
||||
required={requiredValue}
|
||||
InputLabelProps={inputLabelProps}
|
||||
multiline={multiline}
|
||||
maxRows={maxRows}
|
||||
error={localError}
|
||||
type={type}
|
||||
onWheel={ event => event.target.blur() }
|
||||
InputProps={{...InputProps, spellCheck: false}}
|
||||
helperText={localError ? errorText : helperText}
|
||||
style={{
|
||||
padding:0,
|
||||
marginBottom: marginBottom ? marginBottom : "5px",
|
||||
marginTop: marginTop ? marginTop: "5px",
|
||||
display: inline ? "inline-block": "",
|
||||
}}
|
||||
classes={{
|
||||
root: classes.textFieldRoot
|
||||
}} />
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
export default MythicTextField;
|
||||
@@ -1,567 +0,0 @@
|
||||
import React from 'react';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import clsx from 'clsx';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import ManageAccountsIcon from '@mui/icons-material/ManageAccounts';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import List from '@mui/material/List';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import { useReactiveVar } from '@apollo/client';
|
||||
import { menuOpen, FailedRefresh } from '../cache';
|
||||
import { TopAppBarNotifications } from './TopAppBarNotifications';
|
||||
import { EventFeedNotifications } from './EventFeedNotifications';
|
||||
import HelpIcon from '@mui/icons-material/Help';
|
||||
import PhoneCallbackIcon from '@mui/icons-material/PhoneCallback';
|
||||
import {ReactComponent as ReactLogo} from './mythic_red_small.svg';
|
||||
import SpaceDashboardTwoToneIcon from '@mui/icons-material/SpaceDashboardTwoTone';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import ExpandLess from '@mui/icons-material/ExpandLess';
|
||||
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||
import LayersTwoToneIcon from '@mui/icons-material/LayersTwoTone';
|
||||
import TableChartTwoToneIcon from '@mui/icons-material/TableChartTwoTone';
|
||||
import PostAddIcon from '@mui/icons-material/PostAdd';
|
||||
import SupervisorAccountIcon from '@mui/icons-material/SupervisorAccount';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import { Typography } from '@mui/material';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import BarChartIcon from '@mui/icons-material/BarChart';
|
||||
import HeadsetTwoToneIcon from '@mui/icons-material/HeadsetTwoTone';
|
||||
import CodeIcon from '@mui/icons-material/Code';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faBiohazard, faFlagCheckered} from '@fortawesome/free-solid-svg-icons';
|
||||
import AttachmentIcon from '@mui/icons-material/Attachment';
|
||||
import FingerprintIcon from '@mui/icons-material/Fingerprint';
|
||||
import {faSocks} from '@fortawesome/free-solid-svg-icons';
|
||||
import CameraAltTwoToneIcon from '@mui/icons-material/CameraAltTwoTone';
|
||||
import {mythicVersion, mythicUIVersion} from '../index';
|
||||
import {MythicStyledTooltip} from './MythicComponents/MythicStyledTooltip';
|
||||
import VpnKeyIcon from '@mui/icons-material/VpnKey';
|
||||
import ThumbDownTwoTone from '@mui/icons-material/ThumbDownTwoTone';
|
||||
import { MythicDialog } from './MythicComponents/MythicDialog';
|
||||
import {MythicFeedbackDialog} from './MythicComponents/MythicFeedbackDialog';
|
||||
import LocalOfferTwoToneIcon from '@mui/icons-material/LocalOfferTwoTone';
|
||||
import StorageIcon from '@mui/icons-material/Storage';
|
||||
import PublicIcon from '@mui/icons-material/Public';
|
||||
import LightModeTwoToneIcon from '@mui/icons-material/LightModeTwoTone';
|
||||
import DarkModeTwoToneIcon from '@mui/icons-material/DarkModeTwoTone';
|
||||
|
||||
|
||||
const PREFIX = 'TopAppBar';
|
||||
|
||||
const classes = {
|
||||
root: `${PREFIX}-root`,
|
||||
title: `${PREFIX}-title`,
|
||||
hide: `${PREFIX}-hide`,
|
||||
drawer: `${PREFIX}-drawer`,
|
||||
drawerPaper: `${PREFIX}-drawerPaper`,
|
||||
drawerHeader: `${PREFIX}-drawerHeader`,
|
||||
listSubHeader: `${PREFIX}-listSubHeader`,
|
||||
appBar: `${PREFIX}-appBar`,
|
||||
appBarShift: `${PREFIX}-appBarShift`,
|
||||
nested: `${PREFIX}-nested`,
|
||||
mythicElement: `${PREFIX}-mythicElement`
|
||||
};
|
||||
|
||||
const StyledAppBar = styled(AppBar)((
|
||||
{
|
||||
theme
|
||||
}
|
||||
) => ({
|
||||
[`& .${classes.hide}`]: {
|
||||
display: 'none',
|
||||
},
|
||||
[`&.${classes.appBar}`]: {
|
||||
width: "100%",
|
||||
backgroundColor: theme.topAppBarColor,
|
||||
transition: theme.transitions.create(['margin', 'width'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
},
|
||||
[`&.${classes.appBarShift}`]: {
|
||||
maxWidth: `calc(100% - ${drawerWidth}px)`,
|
||||
marginLeft: drawerWidth,
|
||||
transition: theme.transitions.create(['margin', 'width'], {
|
||||
easing: theme.transitions.easing.easeOut,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
const StyledDrawer = styled(Drawer)((
|
||||
{
|
||||
theme
|
||||
}
|
||||
) => ({
|
||||
[`& .${classes.title}`]: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
|
||||
[`&.${classes.drawer}`]: {
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
},
|
||||
|
||||
[`& .${classes.drawerPaper}`]: {
|
||||
width: drawerWidth,
|
||||
},
|
||||
|
||||
[`& .${classes.drawerHeader}`]: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: theme.spacing(0, 1),
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
|
||||
[`& .${classes.listSubHeader}`]: {
|
||||
backgroundColor: theme.palette.listSubHeader.default
|
||||
},
|
||||
|
||||
[`& .${classes.nested}`]: {
|
||||
paddingLeft: theme.spacing(4),
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
const drawerWidth = 240;
|
||||
|
||||
export function TopAppBar(props) {
|
||||
const theme = useTheme();
|
||||
|
||||
const feedbackRef = React.useRef(null);
|
||||
const settingsRef = React.useRef(null);
|
||||
const documentationRef = React.useRef(null);
|
||||
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||
const [documentationAnchorEl, setDocumentationAnchorEl] = React.useState(null);
|
||||
const me = props.me;
|
||||
const isOpen = useReactiveVar(menuOpen);
|
||||
const [openGlobal, setOpenGlobal] = React.useState(false);
|
||||
const [openCreate, setOpenCreate] = React.useState(false);
|
||||
const [openOperations, setOpenOperations] = React.useState(false);
|
||||
const [openData, setOpenData] = React.useState(false);
|
||||
const [openFeedbackForm, setOpenFeedbackForm] = React.useState(false);
|
||||
const handleDrawerOpen = () => {
|
||||
menuOpen(true);
|
||||
};
|
||||
|
||||
const handleDrawerClose = () => {
|
||||
menuOpen(false);
|
||||
};
|
||||
|
||||
const handleMenu = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = (evt) => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
const handleDocumentationMenu = (event) => {
|
||||
setDocumentationAnchorEl(event.currentTarget);
|
||||
};
|
||||
const handleDocumentationClose = (evt) => {
|
||||
setDocumentationAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleToggleGlobal = () => {
|
||||
setOpenGlobal(!openGlobal);
|
||||
}
|
||||
const handleToggleCreate = () => {
|
||||
setOpenCreate(!openCreate);
|
||||
}
|
||||
const handleToggleOperations = () => {
|
||||
setOpenOperations(!openOperations);
|
||||
}
|
||||
const handleToggleData = () => {
|
||||
setOpenData(!openData);
|
||||
}
|
||||
const handleLogout = () => {
|
||||
menuOpen(false);
|
||||
console.log("clicked logout, calling FailedRefresh");
|
||||
FailedRefresh();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{me?.user?.current_operation_id ? (<EventFeedNotifications me={me} />) : null }
|
||||
<StyledAppBar className={clsx(classes.appBar, {[classes.appBarShift]: isOpen,})}>
|
||||
|
||||
{ me?.loggedIn ? (
|
||||
<Toolbar variant="dense" >
|
||||
<IconButton
|
||||
edge="start"
|
||||
className={clsx(isOpen && classes.hide)}
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
disableFocusRipple={true}
|
||||
disableRipple={true}
|
||||
onClick={handleDrawerOpen}
|
||||
>
|
||||
<MenuIcon fontSize={"large"} />
|
||||
</IconButton>
|
||||
<div style={{width: "100%"}}>
|
||||
<MythicStyledTooltip title="C2 Profiles and Payload Types">
|
||||
<IconButton component={Link} to='/new/payloadtypes' color="inherit" size="large" disableFocusRipple={true}
|
||||
disableRipple={true}>
|
||||
<HeadsetTwoToneIcon fontSize={"large"} className="mythicElement"/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title="Payloads">
|
||||
<IconButton component={Link} to='/new/payloads' color="inherit" disableFocusRipple={true}
|
||||
disableRipple={true}>
|
||||
<FontAwesomeIcon size={"lg"} icon={faBiohazard} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title="Search Operation">
|
||||
<IconButton component={Link} to='/new/search?tab=callbacks&searchField=Host&search=' color="inherit" disableFocusRipple={true}
|
||||
disableRipple={true}>
|
||||
<SearchIcon fontSize={"large"} className="mythicElement"/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title="Files">
|
||||
<IconButton
|
||||
component={Link}
|
||||
to='/new/search?searchField=Filename&tab=files&location=Downloads'
|
||||
color="inherit"
|
||||
disableFocusRipple={true}
|
||||
disableRipple={true}>
|
||||
<AttachmentIcon fontSize={"large"} className="mythicElement"/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title="Artifacts">
|
||||
<IconButton
|
||||
disableFocusRipple={true}
|
||||
disableRipple={true}
|
||||
component={Link}
|
||||
to='/new/search?searchField=Artifact&tab=artifacts'
|
||||
color="inherit">
|
||||
<FingerprintIcon fontSize={"large"} className="mythicElement"/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title="Proxies">
|
||||
<IconButton component={Link} to='/new/search?tab=socks' color="inherit" disableFocusRipple={true}
|
||||
disableRipple={true}>
|
||||
<FontAwesomeIcon size={"lg"} icon={faSocks} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title="Screenshots" arrow >
|
||||
<IconButton
|
||||
component={Link}
|
||||
to='/new/search?searchField=Filename&tab=files&location=Screenshots'
|
||||
color="inherit"
|
||||
disableFocusRipple={true}
|
||||
disableRipple={true}
|
||||
>
|
||||
<CameraAltTwoToneIcon fontSize={"large"} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title="Credentials" arrow >
|
||||
<IconButton
|
||||
component={Link}
|
||||
to='/new/search?searchField=Account&tab=credentials'
|
||||
color="inherit"
|
||||
disableFocusRipple={true}
|
||||
disableRipple={true}>
|
||||
<VpnKeyIcon fontSize={"large"} className="mythicElement" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title="Active Callbacks">
|
||||
<IconButton component={Link} to='/new/callbacks' color="inherit" disableFocusRipple={true}
|
||||
disableRipple={true}>
|
||||
<PhoneCallbackIcon fontSize={"large"} className="mythicElement"/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title="Reporting" >
|
||||
<IconButton component={Link} to='/new/reporting' color="inherit" disableFocusRipple={true}
|
||||
disableRipple={true}>
|
||||
<FontAwesomeIcon size={"lg"} icon={faFlagCheckered} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title="MITRE ATT&CK" >
|
||||
<IconButton component={Link} to='/new/mitre' color="inherit" disableFocusRipple={true}
|
||||
disableRipple={true}>
|
||||
<TableChartTwoToneIcon fontSize={"large"} className="mythicElement"/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title="Operation Tags" >
|
||||
<IconButton component={Link} to='/new/tagtypes' color="inherit" disableFocusRipple={true}
|
||||
disableRipple={true}>
|
||||
<LocalOfferTwoToneIcon fontSize={"large"} className="mythicElement"/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<Link style={{display: "inline-flex", alignItems: "center", paddingRight: "10px", color: "white", textDecoration: "none"}} to="/new/operations">
|
||||
{me?.user?.current_operation || "No Operation Set"}
|
||||
</Link>
|
||||
<Menu
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleClose}
|
||||
MenuListProps={{style: {backgroundColor: theme.palette.primary.main, color: "white"}}}
|
||||
>
|
||||
<MenuItem divider={true} style={{display: "block"}} component={Link} to="/new/settings" onClick={handleClose} name="settings">
|
||||
<Typography paragraph={true} variant="caption" style={{marginBottom: "0", color: "white"}}>Signed in as:</Typography>
|
||||
<Typography paragraph={true} variant="body1" style={{marginBottom: "0", fontWeight: 600, color: "white"}}> {me?.user?.username || "" } </Typography>
|
||||
</MenuItem>
|
||||
<MenuItem component={Link} to="/new/login" onClick={handleLogout}>Logout</MenuItem>
|
||||
</Menu>
|
||||
<div style={{display: "inline-flex", justifyContent: "flex-end", float: "right"}}>
|
||||
<IconButton
|
||||
aria-label="feedback"
|
||||
aria-controls="menu-appbar"
|
||||
aria-haspopup="true"
|
||||
onClick={() => setOpenFeedbackForm(true)}
|
||||
ref={feedbackRef}
|
||||
color="inherit"
|
||||
style={{float: "right", }}
|
||||
disableFocusRipple={true}
|
||||
disableRipple={true}>
|
||||
<MythicStyledTooltip title="Send Support Feedback">
|
||||
<ThumbDownTwoTone fontSize={"large"} className="mythicElement" />
|
||||
</MythicStyledTooltip>
|
||||
</IconButton>
|
||||
{openFeedbackForm &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openFeedbackForm}
|
||||
onClose={()=>{setOpenFeedbackForm(false);}}
|
||||
innerDialog={<MythicFeedbackDialog
|
||||
title={"Submit Feedback via Webhook"}
|
||||
onClose={()=>{setOpenFeedbackForm(false);}} />}
|
||||
/>
|
||||
}
|
||||
<IconButton
|
||||
aria-label="documentation links"
|
||||
aria-controls="menu-appbar"
|
||||
aria-haspopup="true"
|
||||
onClick={handleDocumentationMenu}
|
||||
ref={documentationRef}
|
||||
color="inherit"
|
||||
style={{float:"right"}}
|
||||
disableFocusRipple={true}
|
||||
disableRipple={true}>
|
||||
<MythicStyledTooltip title="Help">
|
||||
<HelpIcon fontSize={"large"} className="mythicElement"/>
|
||||
</MythicStyledTooltip>
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="menu-appbar"
|
||||
anchorEl={documentationAnchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
open={Boolean(documentationAnchorEl)}
|
||||
onClose={handleDocumentationClose}
|
||||
MenuListProps={{style: {backgroundColor: theme.palette.primary.main, color: "white"}}}
|
||||
>
|
||||
<MenuItem component={Link} target="_blank" to="/docs/agents" onClick={handleDocumentationClose}>Agent Documentation</MenuItem>
|
||||
<MenuItem component={Link} target="_blank" to="/docs/wrappers" onClick={handleDocumentationClose}>Wrapper Documentation</MenuItem>
|
||||
<MenuItem component={Link} target="_blank" to="/docs/c2-profiles" onClick={handleDocumentationClose}>C2 Profile Documentation</MenuItem>
|
||||
<MenuItem component={Link} to={{pathname: "https://docs.mythic-c2.net"}} target="_blank" onClick={handleDocumentationClose}>Mythic Documentation</MenuItem>
|
||||
</Menu>
|
||||
<TopAppBarNotifications />
|
||||
<IconButton
|
||||
aria-label="account of current user"
|
||||
aria-controls="menu-appbar"
|
||||
aria-haspopup="true"
|
||||
onClick={handleMenu}
|
||||
ref={settingsRef}
|
||||
color="inherit"
|
||||
style={{float: "right", }}
|
||||
disableFocusRipple={true}
|
||||
disableRipple={true} >
|
||||
<MythicStyledTooltip title="Settings or Logout">
|
||||
<ManageAccountsIcon fontSize={"large"} className="mythicElement" />
|
||||
</MythicStyledTooltip>
|
||||
</IconButton>
|
||||
{props.theme === 'dark' &&
|
||||
<IconButton
|
||||
disableFocusRipple={true}
|
||||
disableRipple={true}
|
||||
onClick={props.toggleTheme}
|
||||
style={{float: "right", paddingRight: "0px", color: '#2f0e67'}}
|
||||
|
||||
>
|
||||
<MythicStyledTooltip title="Change to Light Mode">
|
||||
<DarkModeTwoToneIcon fontSize={"large"} className="mythicElement" />
|
||||
</MythicStyledTooltip>
|
||||
</IconButton>
|
||||
}
|
||||
{props.theme === 'light' &&
|
||||
<IconButton
|
||||
disableFocusRipple={true}
|
||||
disableRipple={true}
|
||||
onClick={props.toggleTheme}
|
||||
style={{float: "right", paddingRight: "0px", color: '#eacc1b'}}
|
||||
|
||||
>
|
||||
<MythicStyledTooltip title="Change to Dark Mode">
|
||||
<LightModeTwoToneIcon fontSize={"large"} className="mythicElement" />
|
||||
</MythicStyledTooltip>
|
||||
</IconButton>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Toolbar>
|
||||
) : null
|
||||
}
|
||||
</StyledAppBar>
|
||||
<StyledDrawer
|
||||
className={classes.drawer}
|
||||
anchor="left"
|
||||
open={isOpen}
|
||||
classes={{
|
||||
paper: classes.drawerPaper,
|
||||
}}
|
||||
onClose={handleDrawerClose}
|
||||
>
|
||||
<div className={classes.drawerHeader} role="presentation">
|
||||
<ReactLogo style={{width: "80%", display: "inline-block"}}/>
|
||||
|
||||
<IconButton onClick={handleDrawerClose} size="large">
|
||||
{theme.direction === 'ltr' ? <ChevronLeftIcon className="mythicElement"/> : <ChevronRightIcon className="mythicElement"/>}
|
||||
</IconButton>
|
||||
</div>
|
||||
<Divider />
|
||||
<List
|
||||
subheader={
|
||||
<ListSubheader className={classes.listSubHeader} component="div" id="nested-list-subheader">
|
||||
Home
|
||||
</ListSubheader>
|
||||
}>
|
||||
<div style={{marginLeft: "15px"}}>
|
||||
<b>Mythic Version:</b> v{mythicVersion}<br/>
|
||||
<b>UI Version:</b> v{mythicUIVersion}<br/>
|
||||
</div>
|
||||
|
||||
<ListItem button component={Link} to='/new' key={"home"} onClick={handleDrawerClose}>
|
||||
<ListItemIcon ><SpaceDashboardTwoToneIcon fontSize={"large"} className="mythicElement" /></ListItemIcon>
|
||||
<ListItemText primary={"Dashboard"} />
|
||||
</ListItem>
|
||||
</List>
|
||||
<Divider />
|
||||
<List
|
||||
subheader={
|
||||
<ListSubheader className={classes.listSubHeader} component="div" id="nested-list-subheader">
|
||||
Global Configurations
|
||||
</ListSubheader>
|
||||
}>
|
||||
<ListItem button onClick={handleToggleGlobal}>
|
||||
<ListItemIcon><LayersTwoToneIcon fontSize={"large"} /></ListItemIcon>
|
||||
<ListItemText>Services</ListItemText>
|
||||
{openGlobal ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItem>
|
||||
<Collapse in={openGlobal} unmountOnExit>
|
||||
<List component="div" disablePadding>
|
||||
|
||||
<ListItem button className={classes.nested} target="_blank" component={Link} to='/jupyter' key={"jupyter"} onClick={handleDrawerClose}>
|
||||
<ListItemIcon><CodeIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
|
||||
<ListItemText primary={"Jupyter Notebooks"} />
|
||||
</ListItem>
|
||||
<ListItem button className={classes.nested} target="_blank" component={Link} to='/console' key={"console"} onClick={handleDrawerClose}>
|
||||
<ListItemIcon><StorageIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
|
||||
<ListItemText primary={"GraphQL Console"} />
|
||||
</ListItem>
|
||||
<ListItem button className={classes.nested} component={Link} to='/new/consuming_services' key={"consuming"} onClick={handleDrawerClose}>
|
||||
<ListItemIcon><PublicIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
|
||||
<ListItemText primary={"Consuming Services"} />
|
||||
</ListItem>
|
||||
</List>
|
||||
</Collapse>
|
||||
<ListItem button onClick={handleToggleCreate}>
|
||||
<ListItemIcon><PostAddIcon /></ListItemIcon>
|
||||
<ListItemText>Create</ListItemText>
|
||||
{openCreate ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItem>
|
||||
<Collapse in={openCreate} unmountOnExit>
|
||||
<List component="div" disablePadding>
|
||||
<ListItem button className={classes.nested} component={Link} to='/new/createpayload' key={"createpayload"} onClick={handleDrawerClose} state={{from: 'TopAppBar'}}>
|
||||
<ListItemIcon><PostAddIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
|
||||
<ListItemText primary={"Create Payload"} />
|
||||
</ListItem>
|
||||
<ListItem button className={classes.nested} component={Link} to='/new/createwrapper' key={"createwrapper"} onClick={handleDrawerClose}>
|
||||
<ListItemIcon><PostAddIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
|
||||
<ListItemText primary={"Create Wrapper"} />
|
||||
</ListItem>
|
||||
</List>
|
||||
</Collapse>
|
||||
<ListItem button onClick={handleToggleOperations}>
|
||||
<ListItemIcon><SupervisorAccountIcon fontSize={"large"} /></ListItemIcon>
|
||||
<ListItemText>Operation Config</ListItemText>
|
||||
{openOperations ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItem>
|
||||
<Collapse in={openOperations} unmountOnExit>
|
||||
<List component="div" disablePadding>
|
||||
<ListItem button className={classes.nested} component={Link} to='/new/payloadtypes' key={"payloadtypes"} onClick={handleDrawerClose}>
|
||||
<ListItemIcon><HeadsetTwoToneIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
|
||||
<ListItemText primary={"Agents & C2"} />
|
||||
</ListItem>
|
||||
<ListItem button className={classes.nested} component={Link} to='/new/operations' key={"modifyoperations"} onClick={handleDrawerClose}>
|
||||
<ListItemIcon><EditIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
|
||||
<ListItemText primary={"Modify Operations"} />
|
||||
</ListItem>
|
||||
<ListItem button className={classes.nested} component={Link} to='/new/browserscripts' key={"browserscripts"} onClick={handleDrawerClose}>
|
||||
<ListItemIcon><CodeIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
|
||||
<ListItemText primary={"BrowserScripts"} />
|
||||
</ListItem>
|
||||
</List>
|
||||
</Collapse>
|
||||
</List>
|
||||
<Divider />
|
||||
<List
|
||||
subheader={
|
||||
<ListSubheader className={classes.listSubHeader} component="div" id="nested-list-subheader">
|
||||
Operational Views
|
||||
</ListSubheader>
|
||||
}>
|
||||
<ListItem button onClick={handleToggleData}>
|
||||
<ListItemIcon><BarChartIcon fontSize={"large"} /></ListItemIcon>
|
||||
<ListItemText>Operational Data</ListItemText>
|
||||
{openData ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItem>
|
||||
<Collapse in={openData} unmountOnExit>
|
||||
<List component="div" disablePadding>
|
||||
<ListItem button className={classes.nested} component={Link} to='/new/payloads' key={"payloads"} onClick={handleDrawerClose}>
|
||||
<ListItemIcon><FontAwesomeIcon icon={faBiohazard} size="2x"/></ListItemIcon>
|
||||
<ListItemText primary={"Payloads"} />
|
||||
</ListItem>
|
||||
<ListItem button className={classes.nested} component={Link} to='/new/search?tab=callbacks&searchField=Host&search=' key={"search"} onClick={handleDrawerClose}>
|
||||
<ListItemIcon><SearchIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
|
||||
<ListItemText primary={"Search"} />
|
||||
</ListItem>
|
||||
</List>
|
||||
</Collapse>
|
||||
<ListItem button component={Link} to='/new/callbacks' key={"callbacks"} onClick={handleDrawerClose}>
|
||||
<ListItemIcon><PhoneCallbackIcon fontSize={"large"} className="mythicElement"/></ListItemIcon>
|
||||
<ListItemText primary={"Active Callbacks"} />
|
||||
</ListItem>
|
||||
</List>
|
||||
<Divider />
|
||||
</StyledDrawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import {useSubscription, gql } from '@apollo/client';
|
||||
import Badge from '@mui/material/Badge';
|
||||
import NotificationsActiveTwoToneIcon from '@mui/icons-material/NotificationsActiveTwoTone';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import {alertCount} from "../cache";
|
||||
|
||||
const SUB_Event_Logs = gql`
|
||||
subscription OperationAlertCounts{
|
||||
operation_stream(cursor: {initial_value: {updated_at: "1970-01-01"}, ordering: ASC}, batch_size: 1) {
|
||||
id
|
||||
alert_count
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function TopAppBarVerticalEventLogNotifications(props) {
|
||||
const theme = useTheme();
|
||||
const [alerts, setAlerts] = React.useState(alertCount());
|
||||
const { loading, error } = useSubscription(SUB_Event_Logs, {
|
||||
onError: data => {
|
||||
console.log("vertical event log error")
|
||||
console.error(data);
|
||||
},
|
||||
onData: ({data}) => {
|
||||
|
||||
const newAlertCount = data.data.operation_stream[0].alert_count;
|
||||
if(newAlertCount !== alerts){
|
||||
setAlerts(newAlertCount);
|
||||
alertCount(newAlertCount);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
error ? (
|
||||
<Badge color="secondary" badgeContent={"X"}>
|
||||
<NotificationsActiveTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} />
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge badgeContent={alerts}
|
||||
color="error" max={99}
|
||||
>
|
||||
<NotificationsActiveTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"}/>
|
||||
</Badge>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import React from 'react';
|
||||
import {useSubscription, gql } from '@apollo/client';
|
||||
import Badge from '@mui/material/Badge';
|
||||
import NotificationsActiveTwoToneIcon from '@mui/icons-material/NotificationsActiveTwoTone';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { IconButton } from '@mui/material';
|
||||
import {snackActions} from './utilities/Snackbar';
|
||||
import { meState } from '../cache';
|
||||
import { useReactiveVar } from '@apollo/client';
|
||||
import {MythicStyledTooltip} from "./MythicComponents/MythicStyledTooltip";
|
||||
|
||||
const SUB_Event_Logs = gql`
|
||||
subscription MySubscription($operation_id: Int!) {
|
||||
operation_by_pk(id: $operation_id) {
|
||||
id
|
||||
alert_count
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function TopAppBarNotifications(props) {
|
||||
const me = useReactiveVar(meState);
|
||||
|
||||
const { loading, error, data } = useSubscription(SUB_Event_Logs, {
|
||||
variables: {operation_id: me?.user?.current_operation_id || 0},
|
||||
onError: data => {
|
||||
snackActions.error("Mythic encountered an error getting event log messages: " + data.toString());
|
||||
console.error(data);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<MythicStyledTooltip title="Event Feed">
|
||||
<IconButton
|
||||
color="inherit"
|
||||
component={Link}
|
||||
disableFocusRipple={true}
|
||||
disableRipple={true}
|
||||
to='/new/EventFeed'
|
||||
style={{float: "right"}}>
|
||||
{error ? (
|
||||
<Badge color="secondary" badgeContent={0}>
|
||||
<NotificationsActiveTwoToneIcon fontSize={"large"} />
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge badgeContent={data?.operation_by_pk?.alert_count || 0} color="error">
|
||||
<NotificationsActiveTwoToneIcon fontSize={"large"} />
|
||||
</Badge>
|
||||
)}
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -4,6 +4,7 @@ import {BrowserScriptsTable} from './BrowserScriptsTable';
|
||||
import {snackActions} from '../../utilities/Snackbar';
|
||||
import { Backdrop } from '@mui/material';
|
||||
import {CircularProgress} from '@mui/material';
|
||||
import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
|
||||
|
||||
|
||||
const SUB_BrowserScripts = gql`
|
||||
@@ -78,7 +79,7 @@ export function BrowserScripts({me}){
|
||||
}
|
||||
} )
|
||||
setBrowserScripts(scripts);
|
||||
if(backdropOpen){setBackdropOpen(false);}
|
||||
setBackdropOpen(false);
|
||||
}
|
||||
});
|
||||
const [toggleActive] = useMutation(updateBrowserScriptActive, {
|
||||
@@ -117,6 +118,7 @@ export function BrowserScripts({me}){
|
||||
}
|
||||
});
|
||||
const onToggleActive = ({browserscript_id, active}) => {
|
||||
setBackdropOpen(true);
|
||||
toggleActive({variables: {browserscript_id, active}});
|
||||
}
|
||||
const onSubmitEdit = ({browserscript_id, script, command_id, payload_type_id}) => {
|
||||
@@ -134,7 +136,7 @@ export function BrowserScripts({me}){
|
||||
}
|
||||
}, [])
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicPageBody>
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={false}>
|
||||
<CircularProgress color="inherit" />
|
||||
</Backdrop>
|
||||
@@ -144,6 +146,6 @@ export function BrowserScripts({me}){
|
||||
onSubmitEdit={onSubmitEdit} onRevert={onRevert} onSubmitNew={onSubmitCreateNewBrowserScript}
|
||||
/>
|
||||
|
||||
</React.Fragment>
|
||||
</MythicPageBody>
|
||||
);
|
||||
}
|
||||
@@ -9,33 +9,33 @@ import Paper from '@mui/material/Paper';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { BrowserScriptsTableRow } from './BrowserScriptsTableRow';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined';
|
||||
import AddCircleIcon from '@mui/icons-material/AddCircle';
|
||||
import Button from '@mui/material/Button';
|
||||
import { MythicDialog } from '../../MythicComponents/MythicDialog';
|
||||
import {EditScriptDialog} from './EditScriptDialog';
|
||||
import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
|
||||
|
||||
|
||||
export function BrowserScriptsTable(props){
|
||||
const theme = useTheme();
|
||||
const [openNewScriptDialog, setOpenNewScriptDialog] = React.useState(false);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main, marginBottom: "5px", marginTop: "10px", marginRight: "5px"}} variant={"elevation"}>
|
||||
<Typography variant="h3" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
|
||||
Browser Scripts
|
||||
</Typography>
|
||||
<Button size="small" onClick={() => setOpenNewScriptDialog(true)} style={{float: "right", marginTop: "10px", marginRight: "30px"}} startIcon={<AddCircleOutlineOutlinedIcon/>} color="primary" variant="contained">New Script</Button>
|
||||
{openNewScriptDialog ? (
|
||||
<>
|
||||
<MythicPageHeader title={"Browser Scripts"}>
|
||||
<Button size="small" onClick={() => setOpenNewScriptDialog(true)} style={{whiteSpace: "nowrap", color: "white"}}
|
||||
startIcon={<AddCircleIcon color={"success"} style={{backgroundColor: "white", borderRadius: "10px"}}/>} >
|
||||
New Script
|
||||
</Button>
|
||||
{openNewScriptDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openNewScriptDialog}
|
||||
onClose={()=>{setOpenNewScriptDialog(false);}}
|
||||
innerDialog={
|
||||
<EditScriptDialog me={props.me} onClose={()=>{setOpenNewScriptDialog(false);}} title="Create New Browser Script" new={true} onSubmitEdit={props.onSubmitNew} />
|
||||
} />
|
||||
) : null
|
||||
} />
|
||||
}
|
||||
</Paper>
|
||||
<TableContainer component={Paper} className="mythicElement" style={{maxHeight: "90%"}}>
|
||||
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
</MythicPageHeader>
|
||||
<TableContainer className="mythicElement" style={{height: "100%", flexGrow: 1}}>
|
||||
<Table stickyHeader={true} size="small" style={{"tableLayout": "fixed", "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{width: "3rem"}}>Edit</TableCell>
|
||||
@@ -58,8 +58,8 @@ export function BrowserScriptsTable(props){
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</React.Fragment>
|
||||
</TableContainer>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {EditScriptDialog} from './EditScriptDialog';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
|
||||
|
||||
export function BrowserScriptsTableRow(props){
|
||||
const [openEdit, setOpenEdit] = React.useState(false);
|
||||
@@ -29,17 +30,14 @@ export function BrowserScriptsTableRow(props){
|
||||
<Switch
|
||||
checked={props.active}
|
||||
onChange={onToggleActive}
|
||||
color="primary"
|
||||
color="info"
|
||||
inputProps={{ 'aria-label': 'checkbox', "track": "white" }}
|
||||
name="Active"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MythicStyledTooltip title={props.payloadtype.name}>
|
||||
<img
|
||||
style={{width: "35px", height: "35px"}}
|
||||
src={"/static/" + props.payloadtype.name + ".svg"}
|
||||
/>
|
||||
<MythicAgentSVGIcon payload_type={props.payloadtype.name} style={{width: "35px", height: "35px"}} />
|
||||
</MythicStyledTooltip>
|
||||
</TableCell>
|
||||
<TableCell>{props.command.cmd}</TableCell>
|
||||
|
||||
@@ -111,7 +111,6 @@ export function EditScriptDialog(props) {
|
||||
//setScript(atob(props.script));
|
||||
setScript(decodeURIComponent(window.atob(props.script)));
|
||||
}catch(error){
|
||||
console.log(error)
|
||||
setScript(props.script);
|
||||
}
|
||||
}
|
||||
@@ -270,7 +269,7 @@ export function EditScriptDialog(props) {
|
||||
input={<Input style={{width: "100%"}}/>}
|
||||
>
|
||||
{availableTasks.map( (opt) => (
|
||||
<MenuItem value={opt} key={"task" + opt.id}>{opt.command_name + " " + opt.display_params}</MenuItem>
|
||||
<MenuItem value={opt} key={"task" + opt.id}>{opt.command_name + " / " + opt.display_id + " / " + opt.display_params}</MenuItem>
|
||||
) )}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
@@ -12,6 +12,7 @@ import CardHeader from '@mui/material/CardHeader';
|
||||
import {gql, useQuery} from '@apollo/client';
|
||||
import { CardContent } from '@mui/material';
|
||||
import {classes, StyledButton, StyledDivider} from '../../MythicComponents/MythicTransferList';
|
||||
import {MythicDraggableDialogTitle} from "../../MythicComponents/MythicDraggableDialogTitle";
|
||||
|
||||
const getCommandsQuery = gql`
|
||||
query GetCallbackDetails($callback_id: Int!) {
|
||||
@@ -19,85 +20,130 @@ query GetCallbackDetails($callback_id: Int!) {
|
||||
loadedcommands {
|
||||
command {
|
||||
cmd
|
||||
payloadtype {
|
||||
name
|
||||
}
|
||||
id
|
||||
}
|
||||
id
|
||||
}
|
||||
payload {
|
||||
payloadtype {
|
||||
commands {
|
||||
name
|
||||
commands(where: {deleted: {_eq: false}}) {
|
||||
cmd
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
command(where: {deleted: {_eq: false}, payloadtype: {agent_type: {_eq: "command_augment"}}}){
|
||||
cmd
|
||||
id
|
||||
payloadtype {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const CustomListElement = React.memo(({valueObj, handleToggle}) => {
|
||||
return (
|
||||
<ListItem style={{padding: 0}} key={valueObj.display_value} role="listitem" button
|
||||
onClick={() => handleToggle(valueObj)}>
|
||||
<ListItemIcon>
|
||||
<Checkbox
|
||||
checked={valueObj.checked}
|
||||
disableRipple
|
||||
inputProps={{'aria-labelledby': valueObj.label}}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText id={valueObj.label} primary={valueObj.display_value}/>
|
||||
</ListItem>
|
||||
)
|
||||
});
|
||||
const CustomList = ({title, items, handleToggle}) => {
|
||||
const renderedList = React.useMemo( () => {
|
||||
return items.map((valueObj) => (
|
||||
<CustomListElement key={valueObj.display_value} valueObj={valueObj} handleToggle={handleToggle} />
|
||||
))
|
||||
}, [items, handleToggle]);
|
||||
return (
|
||||
<>
|
||||
<CardHeader className={classes.cardHeader} title={title}/>
|
||||
<StyledDivider classes={{root: classes.divider}}/>
|
||||
<CardContent style={{flexGrow: 1, overflowY: "auto", padding: 0}}>
|
||||
<List dense component="div" role="list" style={{padding: 0, width: "100%"}}>
|
||||
{renderedList}
|
||||
</List>
|
||||
</CardContent>
|
||||
</>
|
||||
)
|
||||
};
|
||||
export function AddRemoveCallbackCommandsDialog(props) {
|
||||
|
||||
const [checked, setChecked] = React.useState([]);
|
||||
const [left, setLeft] = React.useState([]);
|
||||
const [originalLeft, setOriginalLeft] = React.useState([]);
|
||||
const [originalRight, setOriginalRight] = React.useState([]);
|
||||
const [right, setRight] = React.useState([]);
|
||||
const [leftTitle, setLeftTitle] = React.useState("Commands Not Included");
|
||||
const [rightTitle, setRightTitle] = React.useState("Commands Included");
|
||||
const leftChecked = intersection(checked, left);
|
||||
const rightChecked = intersection(checked, right);
|
||||
const leftTitle = "Commands Not Included";
|
||||
const rightTitle = "Commands Included";
|
||||
|
||||
useQuery(getCommandsQuery, {variables: {callback_id: props.callback_id},
|
||||
fetchPolicy: "no-cache",
|
||||
onCompleted: (data) => {
|
||||
setOriginalLeft(data.callback_by_pk.payload.payloadtype.commands);
|
||||
let originalLeftFromQuery = data.callback_by_pk.payload.payloadtype.commands.map(c => {
|
||||
return {...c, payloadtype: {name: data.callback_by_pk.payload.payloadtype.name}}
|
||||
})
|
||||
originalLeftFromQuery = [...originalLeftFromQuery, ...data.command];
|
||||
setOriginalRight(data.callback_by_pk.loadedcommands);
|
||||
const leftData = data.callback_by_pk.payload.payloadtype.commands.reduce( (prev, cur) => {
|
||||
if( data.callback_by_pk.loadedcommands.filter(c => c.command.cmd === cur.cmd).length === 0){
|
||||
|
||||
let leftData = originalLeftFromQuery.reduce( (prev, cur) => {
|
||||
if( data.callback_by_pk.loadedcommands.filter(c => c.command.cmd === cur.cmd && c.command.payloadtype.name === cur.payloadtype.name).length === 0){
|
||||
return [...prev, cur];
|
||||
} else {
|
||||
return [...prev];
|
||||
}
|
||||
}, []);
|
||||
|
||||
leftData.sort( (a,b) => a.cmd < b.cmd ? -1 : 1);
|
||||
leftData = leftData.map( c => {return {...c,
|
||||
checked: false,
|
||||
display_value: c.cmd + " (" + c?.payloadtype?.name + ")",
|
||||
label: `transfer-list-item-${c.cmd + " (" + c?.payloadtype?.name + ")"}-label`
|
||||
}});
|
||||
setLeft(leftData);
|
||||
const rightData = data.callback_by_pk.loadedcommands.map( c => c.command);
|
||||
let rightData = data.callback_by_pk.loadedcommands.map( c => c.command);
|
||||
rightData.sort( (a,b) => a.cmd < b.cmd ? -1 : 1);
|
||||
rightData = rightData.map( c => {return {...c,
|
||||
checked: false,
|
||||
display_value: c.cmd + " (" + c?.payloadtype?.name + ")",
|
||||
label: `transfer-list-item-${c.cmd + " (" + c?.payloadtype?.name + ")"}-label`
|
||||
}});
|
||||
setRight(rightData);
|
||||
},
|
||||
onError: (data) => {
|
||||
|
||||
}
|
||||
})
|
||||
function not(a, b) {
|
||||
if(props.itemKey){
|
||||
return a.filter( (value) => b.find( (element) => element[props.itemKey] === value[props.itemKey] ) === undefined)
|
||||
}
|
||||
return a.filter((value) => b.indexOf(value) === -1);
|
||||
}
|
||||
|
||||
function intersection(a, b) {
|
||||
if(props.itemKey){
|
||||
return a.filter( (value) => b.find( (element) => element[props.itemKey] === value[props.itemKey] ) !== undefined)
|
||||
}
|
||||
return a.filter((value) => b.indexOf(value) !== -1);
|
||||
}
|
||||
const handleToggle = (value) => () => {
|
||||
let currentIndex = -1;
|
||||
if(props.itemKey){
|
||||
currentIndex = checked.findIndex( (element) => element[props.itemKey] === value[props.itemKey]);
|
||||
}else{
|
||||
currentIndex = checked.indexOf(value);
|
||||
}
|
||||
|
||||
const newChecked = [...checked];
|
||||
|
||||
if (currentIndex === -1) {
|
||||
newChecked.push(value);
|
||||
} else {
|
||||
newChecked.splice(currentIndex, 1);
|
||||
const handleToggle = (value) => {
|
||||
let found = false;
|
||||
const newLeft = left.map( c => {
|
||||
if(c.display_value === value.display_value){
|
||||
c.checked = !c.checked;
|
||||
found = true;
|
||||
}
|
||||
return c;
|
||||
});
|
||||
if(found){
|
||||
setLeft(newLeft);
|
||||
return
|
||||
}
|
||||
|
||||
setChecked(newChecked);
|
||||
const newRight = right.map( c => {
|
||||
if(c.display_value === value.display_value){
|
||||
c.checked = !c.checked;
|
||||
}
|
||||
return c;
|
||||
});
|
||||
setRight(newRight);
|
||||
};
|
||||
|
||||
const handleAllRight = () => {
|
||||
@@ -106,70 +152,47 @@ export function AddRemoveCallbackCommandsDialog(props) {
|
||||
};
|
||||
|
||||
const handleCheckedRight = () => {
|
||||
setRight(right.concat(leftChecked));
|
||||
setLeft(not(left, leftChecked));
|
||||
setChecked(not(checked, leftChecked));
|
||||
let leftChecked = left.filter(c => c.checked);
|
||||
leftChecked = leftChecked.map(c => {return {...c, checked: false}})
|
||||
let leftNotChecked = left.filter(c => !c.checked);
|
||||
setRight(right.concat(leftChecked));
|
||||
setLeft(leftNotChecked);
|
||||
};
|
||||
|
||||
const handleCheckedLeft = () => {
|
||||
setLeft(left.concat(rightChecked));
|
||||
setRight(not(right, rightChecked));
|
||||
setChecked(not(checked, rightChecked));
|
||||
let rightChecked = right.filter(c => c.checked);
|
||||
rightChecked = rightChecked.map(c => {return {...c, checked: false}})
|
||||
let rightNotChecked = right.filter(c => !c.checked);
|
||||
setLeft(left.concat(rightChecked));
|
||||
setRight(rightNotChecked);
|
||||
};
|
||||
|
||||
const handleAllLeft = () => {
|
||||
setLeft(left.concat(right));
|
||||
setRight([]);
|
||||
};
|
||||
const customList = (title, items) => (
|
||||
<>
|
||||
<CardHeader className={classes.cardHeader} title={title} />
|
||||
<StyledDivider classes={{root: classes.divider}}/>
|
||||
<CardContent style={{flexGrow: 1, overflowY: "auto", padding: 0}}>
|
||||
<List dense component="div" role="list" style={{padding:0, width: "100%"}}>
|
||||
{items.map((valueObj) => {
|
||||
const value = valueObj.cmd;
|
||||
const labelId = `transfer-list-item-${value}-label`;
|
||||
return (
|
||||
<ListItem style={{padding:0}} key={value} role="listitem" button onClick={handleToggle(valueObj)}>
|
||||
<ListItemIcon>
|
||||
<Checkbox
|
||||
checked={checked.findIndex( (element) => element.cmd === value) !== -1}
|
||||
tabIndex={-1}
|
||||
disableRipple
|
||||
inputProps={{ 'aria-labelledby': labelId }}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText id={labelId} primary={value} />
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
<ListItem />
|
||||
</List>
|
||||
</CardContent>
|
||||
</>
|
||||
);
|
||||
|
||||
const setFinalTags = () => {
|
||||
// things to add are in the `right` now but weren't for `originalRight`
|
||||
const commandsToAdd = right.filter( (command) => {
|
||||
return originalRight.filter(orig => orig.command.cmd == command.cmd).length == 0;
|
||||
return originalRight.filter(orig => orig.command.cmd === command.cmd && orig.command.payloadtype.name === command.payloadtype.name).length === 0;
|
||||
});
|
||||
const commandsToRemove = originalRight.filter( (command) => {
|
||||
return right.filter(newCommand => newCommand.cmd == command.command.cmd).length == 0;
|
||||
return right.filter(newCommand => newCommand.cmd === command.command.cmd && newCommand.payloadtype.name === command.command.payloadtype.name).length === 0;
|
||||
})
|
||||
props.onSubmit({commandsToAdd, commandsToRemove});
|
||||
props.onClose();
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Add or Remove Commands for Callback {props.display_id} </DialogTitle>
|
||||
<DialogContent dividers={true} style={{height: "100%", display: "flex", flexDirection: "column", position: "relative", maxHeight: "100%"}}>
|
||||
<DialogTitle>Add or Remove Commands for Callback {props.display_id} </DialogTitle>
|
||||
<DialogContent dividers={true} style={{height: "100%", display: "flex", flexDirection: "column", position: "relative", overflowY: "auto"}}>
|
||||
This will add or remove commands associated with this callback from Mythic's perspective.
|
||||
This does NOT add or remove commands within the payload itself that's beaconing out to Mythic.
|
||||
<div style={{display: "flex", flexDirection: "row", overflowY: "auto", flexGrow: 1, minHeight: 0}}>
|
||||
<div style={{paddingLeft: 0, flexGrow: 1, marginLeft: 0, marginRight: "10px", position: "relative", overflowY: "auto", display: "flex", flexDirection: "column" }}>
|
||||
{customList(leftTitle, left)}
|
||||
</div>
|
||||
<div style={{paddingLeft: 0, flexGrow: 1, marginLeft: 0, marginRight: "10px", position: "relative", overflowY: "auto", display: "flex", flexDirection: "column" }}>
|
||||
<CustomList title={leftTitle} items={left} handleToggle={handleToggle}/>
|
||||
</div>
|
||||
<div style={{display: "flex", flexDirection: "column", justifyContent: "center"}}>
|
||||
<StyledButton
|
||||
variant="contained"
|
||||
@@ -186,7 +209,6 @@ export function AddRemoveCallbackCommandsDialog(props) {
|
||||
size="small"
|
||||
className={classes.button}
|
||||
onClick={handleCheckedRight}
|
||||
disabled={leftChecked.length === 0}
|
||||
aria-label="move selected right"
|
||||
>
|
||||
>
|
||||
@@ -196,7 +218,6 @@ export function AddRemoveCallbackCommandsDialog(props) {
|
||||
size="small"
|
||||
className={classes.button}
|
||||
onClick={handleCheckedLeft}
|
||||
disabled={rightChecked.length === 0}
|
||||
aria-label="move selected left"
|
||||
>
|
||||
<
|
||||
@@ -213,8 +234,8 @@ export function AddRemoveCallbackCommandsDialog(props) {
|
||||
</StyledButton>
|
||||
|
||||
</div>
|
||||
<div style={{marginLeft: "10px", position: "relative", flexGrow: 1, display: "flex", flexDirection: "column" }}>
|
||||
{customList(rightTitle, right)}
|
||||
<div style={{marginLeft: "10px", position: "relative", flexGrow: 1, display: "flex", flexDirection: "column" }}>
|
||||
<CustomList title={rightTitle} items={right} handleToggle={handleToggle} />
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -16,6 +16,14 @@ mutation hideCallback ($callback_display_id: Int!){
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const updateCallbackTriggerMutation = gql`
|
||||
mutation updateCallbackTriggerMutation ($callback_display_id: Int!, $trigger_on_checkin_after_time: Int!){
|
||||
updateCallback(input: {callback_display_id: $callback_display_id, trigger_on_checkin_after_time: $trigger_on_checkin_after_time}) {
|
||||
status
|
||||
error
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const toggleHideCallbackMutations = gql`
|
||||
mutation hideCallback ($callback_display_id: Int!, $active: Boolean!){
|
||||
updateCallback(input: {callback_display_id: $callback_display_id, active: $active}) {
|
||||
@@ -59,8 +67,8 @@ mutation lockCallack($callback_display_id: Int!){
|
||||
}
|
||||
`;
|
||||
export const updateDescriptionCallbackMutation = gql`
|
||||
mutation updateDescriptionCallack($callback_display_id: Int!, $description: String!){
|
||||
updateCallback(input: {callback_display_id: $callback_display_id, description: $description}) {
|
||||
mutation updateDescriptionCallback($callback_display_id: Int!, $description: String!, $color: String!){
|
||||
updateCallback(input: {callback_display_id: $callback_display_id, description: $description, color: $color}) {
|
||||
status
|
||||
error
|
||||
}
|
||||
@@ -87,6 +95,8 @@ export const taskingDataFragment = gql`
|
||||
fragment taskData on task {
|
||||
comment
|
||||
parent_task_id
|
||||
agent_task_id
|
||||
tasking_location
|
||||
callback {
|
||||
display_id
|
||||
id
|
||||
@@ -96,11 +106,23 @@ export const taskingDataFragment = gql`
|
||||
domain
|
||||
ip
|
||||
mythictree_groups
|
||||
pid
|
||||
}
|
||||
callback_id
|
||||
commentOperator{
|
||||
username
|
||||
}
|
||||
eventstepinstance {
|
||||
eventgroupinstance {
|
||||
eventgroup{
|
||||
id
|
||||
}
|
||||
id
|
||||
}
|
||||
eventstep {
|
||||
name
|
||||
}
|
||||
}
|
||||
completed
|
||||
id
|
||||
display_id
|
||||
@@ -111,20 +133,37 @@ export const taskingDataFragment = gql`
|
||||
display_params
|
||||
status
|
||||
timestamp
|
||||
status_timestamp_submitted
|
||||
status_timestamp_processing
|
||||
status_timestamp_preprocessing
|
||||
command {
|
||||
cmd
|
||||
supported_ui_features
|
||||
id
|
||||
payloadtype {
|
||||
name
|
||||
}
|
||||
}
|
||||
command_name
|
||||
opsec_pre_blocked
|
||||
opsec_pre_bypassed
|
||||
opsec_pre_bypass_user {
|
||||
id
|
||||
username
|
||||
}
|
||||
opsec_post_blocked
|
||||
opsec_post_bypassed
|
||||
opsec_post_bypass_user {
|
||||
id
|
||||
username
|
||||
}
|
||||
is_interactive_task
|
||||
interactive_task_type
|
||||
has_intercepted_response
|
||||
tasks(where: {is_interactive_task: {_eq: false}}, order_by: {id: asc}) {
|
||||
id
|
||||
comment
|
||||
agent_task_id
|
||||
commentOperator{
|
||||
username
|
||||
}
|
||||
@@ -137,6 +176,7 @@ export const taskingDataFragment = gql`
|
||||
domain
|
||||
ip
|
||||
mythictree_groups
|
||||
pid
|
||||
}
|
||||
completed
|
||||
subtask_group_name
|
||||
@@ -166,6 +206,7 @@ export const taskingDataFragment = gql`
|
||||
tasks(order_by: {id: asc}) {
|
||||
id
|
||||
}
|
||||
has_intercepted_response
|
||||
}
|
||||
response_count
|
||||
tags {
|
||||
@@ -182,8 +223,8 @@ export const taskingDataFragment = gql`
|
||||
}
|
||||
`;
|
||||
export const createTaskingMutation = gql`
|
||||
mutation createTasking($callback_id: Int, $callback_ids: [Int], $command: String!, $params: String!, $files: [String], $token_id: Int, $tasking_location: String, $original_params: String, $parameter_group_name: String, $parent_task_id: Int, $is_interactive_task: Boolean, $interactive_task_type: Int) {
|
||||
createTask(callback_id: $callback_id, callback_ids: $callback_ids, command: $command, params: $params, files: $files, token_id: $token_id, tasking_location: $tasking_location, original_params: $original_params, parameter_group_name: $parameter_group_name, parent_task_id: $parent_task_id, is_interactive_task: $is_interactive_task, interactive_task_type: $interactive_task_type) {
|
||||
mutation createTasking($callback_id: Int, $callback_ids: [Int], $command: String!, $params: String!, $files: [String], $token_id: Int, $tasking_location: String, $original_params: String, $parameter_group_name: String, $parent_task_id: Int, $is_interactive_task: Boolean, $interactive_task_type: Int, $payload_type: String) {
|
||||
createTask(callback_id: $callback_id, callback_ids: $callback_ids, command: $command, params: $params, files: $files, token_id: $token_id, tasking_location: $tasking_location, original_params: $original_params, parameter_group_name: $parameter_group_name, parent_task_id: $parent_task_id, is_interactive_task: $is_interactive_task, interactive_task_type: $interactive_task_type, payload_type: $payload_type) {
|
||||
status
|
||||
id
|
||||
error
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { CallbacksTabs } from './CallbacksTabs';
|
||||
import SpeedDial from '@mui/material/SpeedDial';
|
||||
import SpeedDialIcon from '@mui/material/SpeedDialIcon';
|
||||
import SpeedDialAction from '@mui/material/SpeedDialAction';
|
||||
import TocIcon from '@mui/icons-material/Toc';
|
||||
import AssessmentIcon from '@mui/icons-material/Assessment';
|
||||
import { CallbacksTop } from './CallbacksTop';
|
||||
@@ -11,53 +7,52 @@ import Split from 'react-split';
|
||||
import PhoneForwardedIcon from '@mui/icons-material/PhoneForwarded';
|
||||
import {MythicDialog} from "../../MythicComponents/MythicDialog";
|
||||
import {ImportCallbackConfigDialog} from "./ImportCallbackConfigDialog";
|
||||
import {reorder} from "../../MythicComponents/MythicDraggableList";
|
||||
import Paper from '@mui/material/Paper';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
|
||||
const PREFIX = 'Callbacks';
|
||||
|
||||
const classes = {
|
||||
root: `${PREFIX}-root`,
|
||||
speedDial: `${PREFIX}-speedDial`,
|
||||
speedDialAction: `${PREFIX}-speedDialAction`,
|
||||
tooltip: `${PREFIX}-tooltip`,
|
||||
arrow: `${PREFIX}-arrow`
|
||||
};
|
||||
|
||||
const StyledSpeedDial = styled(SpeedDial)(({theme}) => ({
|
||||
[`&.${classes.speedDial}`]: {
|
||||
position: 'absolute',
|
||||
'&.MuiSpeedDial-directionUp, &.MuiSpeedDial-directionLeft': {
|
||||
bottom: theme.spacing(2),
|
||||
right: theme.spacing(2),
|
||||
},
|
||||
'&.MuiSpeedDial-directionDown, &.MuiSpeedDial-directionRight': {
|
||||
top: theme.spacing(2),
|
||||
right: theme.spacing(2),
|
||||
},
|
||||
zIndex: 4,
|
||||
},
|
||||
|
||||
[`& .${classes.speedDialAction}`]: {
|
||||
backgroundColor: theme.palette.speedDialAction,
|
||||
},
|
||||
|
||||
[`& .${classes.tooltip}`]: {
|
||||
backgroundColor: theme.palette.background.contrast,
|
||||
color: theme.palette.text.contrast,
|
||||
boxShadow: theme.shadows[1],
|
||||
fontSize: 13,
|
||||
},
|
||||
|
||||
[`& .${classes.arrow}`]: {
|
||||
color: theme.palette.background.contrast,
|
||||
export const getCallbackIdFromClickedTab = (tabId) => {
|
||||
if(tabId === null || tabId === undefined){return 0}
|
||||
if(tabId === ""){return 0}
|
||||
if(tabId.includes("fileBrowser")) {
|
||||
return Number(tabId.split("fileBrowser")[0]);
|
||||
}else if(tabId.includes("interact")){
|
||||
return Number(tabId.split("interact")[0]);
|
||||
}else if(tabId.includes("processBrowser")) {
|
||||
return Number(tabId.split("processBrowser")[0]);
|
||||
}else if(tabId.includes("customFileBasedBrowser")) {
|
||||
return Number(tabId.split("customFileBasedBrowser")[0]);
|
||||
}else if(tabId.includes("customProcessBasedBrowser")) {
|
||||
return Number(tabId.split("customProcessBasedBrowser")[0]);
|
||||
} else {
|
||||
console.log("unknown tab type", tabId);
|
||||
return 0;
|
||||
}
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
export function Callbacks({me}) {
|
||||
const [openCallbackImport, setOpenCallbackImport] = React.useState(false);
|
||||
const [topDisplay, setTopDisplay] = React.useState('table');
|
||||
const [newDataForTab, setNewDataForTab] = React.useState({});
|
||||
const [openTabs, setOpenTabs] = React.useState([]);
|
||||
const [clickedTabId, setClickedTabId] = React.useState('');
|
||||
const [clickedTabId, setClickedTabIdValue] = React.useState('');
|
||||
const openTabRef = React.useRef([]);
|
||||
const callbackTableGridRef = React.useRef();
|
||||
const [callbackTableSplitSizes, setCallbackTableSplitSizes] = React.useState([30, 70]);
|
||||
const setClickedTabId = (tabID) => {
|
||||
if(callbackTableGridRef.current){
|
||||
let tabIDNumber = getCallbackIdFromClickedTab(tabID);
|
||||
let rowIndex = callbackTableGridRef.current?.props?.itemData?.items?.findIndex((e) => {
|
||||
return e[0]?.props?.rowData?.id === tabIDNumber
|
||||
});
|
||||
if(rowIndex >= 0){
|
||||
callbackTableGridRef.current?.scrollToItem({rowIndex: rowIndex, align: "end", columnIndex: 0})
|
||||
}
|
||||
}
|
||||
setClickedTabIdValue(tabID);
|
||||
setNewDataForTab((prev) => {return {...prev, [tabID]: false}});
|
||||
}
|
||||
useEffect(() => {
|
||||
const oldTabs = localStorage.getItem('openTabs');
|
||||
if (oldTabs !== undefined && oldTabs !== null) {
|
||||
@@ -72,12 +67,26 @@ export function Callbacks({me}) {
|
||||
console.log('failed to parse oldTabs', error);
|
||||
}
|
||||
}
|
||||
const oldSizes = localStorage.getItem("callbackTableSplitSizes");
|
||||
if (oldSizes) {
|
||||
try{
|
||||
setCallbackTableSplitSizes(JSON.parse(oldSizes));
|
||||
}catch(error){
|
||||
console.log("failed to parse callback table split sizes");
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
useEffect( () => {
|
||||
openTabRef.current = openTabs;
|
||||
}, [openTabs])
|
||||
const onOpenTab = React.useRef( (tabData) => {
|
||||
let found = false;
|
||||
openTabRef.current = openTabRef.current.map( (tab) => {
|
||||
if(tab.tabID === tabData.tabID){
|
||||
return {...tabData};
|
||||
}
|
||||
return {...tab};
|
||||
})
|
||||
openTabRef.current.forEach((tab) => {
|
||||
if (tab.tabID === tabData.tabID) found = true;
|
||||
});
|
||||
@@ -86,10 +95,29 @@ export function Callbacks({me}) {
|
||||
const tabs = [...openTabRef.current, { ...tabData }];
|
||||
localStorage.setItem('openTabs', JSON.stringify(tabs));
|
||||
setOpenTabs(tabs);
|
||||
} else {
|
||||
setOpenTabs([...openTabRef.current]);
|
||||
}
|
||||
localStorage.setItem('clickedTab', tabData.tabID);
|
||||
setClickedTabId(tabData.tabID);
|
||||
|
||||
});
|
||||
const onOpenTabs = React.useRef( (tabData) => {
|
||||
let currentTabs = [...openTabRef.current];
|
||||
for(let i = 0; i < tabData.length; i++){
|
||||
let found = false;
|
||||
currentTabs.forEach((tab) => {
|
||||
if (tab.tabID === tabData[i].tabID) found = true;
|
||||
});
|
||||
if (!found) {
|
||||
currentTabs = [...currentTabs, { ...tabData[i] }];
|
||||
}
|
||||
}
|
||||
localStorage.setItem('openTabs', JSON.stringify(currentTabs));
|
||||
setOpenTabs(currentTabs);
|
||||
localStorage.setItem('clickedTab', tabData[0].tabID);
|
||||
setClickedTabId(tabData[0].tabID);
|
||||
|
||||
});
|
||||
const onEditTabDescription = React.useCallback( (tabInfo, description) => {
|
||||
const tabs = openTabs.map((t) => {
|
||||
@@ -108,6 +136,10 @@ export function Callbacks({me}) {
|
||||
});
|
||||
localStorage.setItem('openTabs', JSON.stringify(tabSet));
|
||||
setOpenTabs(tabSet);
|
||||
if(tabSet.length === 0){
|
||||
setClickedTabId("0");
|
||||
localStorage.removeItem("clickedTab");
|
||||
}
|
||||
}, [openTabs]);
|
||||
const onDragTab = ({selected, toLeftOf}) => {
|
||||
//console.log("onDragTab in CallbacksTabs", selected, toLeftOf);
|
||||
@@ -136,7 +168,7 @@ export function Callbacks({me}) {
|
||||
newOpenTabList.push(openTabs[targetTabIndex]);
|
||||
newOpenTabList.push(openTabs[i]);
|
||||
}
|
||||
|
||||
|
||||
setClickedTabId(openTabs[targetTabIndex].tabID)
|
||||
} else {
|
||||
newOpenTabList.push(openTabs[i]);
|
||||
@@ -169,121 +201,73 @@ export function Callbacks({me}) {
|
||||
}
|
||||
},
|
||||
];
|
||||
const onDragEnd = ({ destination, source }) => {
|
||||
// dropped outside the list
|
||||
if (!destination) return;
|
||||
const newItems = reorder(openTabs, source.index, destination.index);
|
||||
setOpenTabs(newItems);
|
||||
localStorage.setItem('openTabs', JSON.stringify(newItems));
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<SpeedDialWrapper setTopDisplay={setTopDisplay} />
|
||||
<Split direction="vertical" sizes={[30, 70]} minSize={[0,0]} style={{ height: "100%" }}>
|
||||
<div className="bg-gray-base">
|
||||
<CallbacksTop topDisplay={topDisplay} onOpenTab={onOpenTab.current} me={me}/>
|
||||
<Split direction="vertical"
|
||||
sizes={callbackTableSplitSizes}
|
||||
minSize={[0,0]}
|
||||
onDragEnd={(sizes) => localStorage.setItem('callbackTableSplitSizes', JSON.stringify(sizes))}
|
||||
style={{ height: "100%" }}>
|
||||
<div style={{display: "flex", flexDirection: "row-reverse"}}>
|
||||
<Paper elevation={5} style={{width: "30px", display: "flex", flexDirection: "column", alignItems: "center", overflow: "hidden",
|
||||
backgroundColor: "transparent"}}>
|
||||
{topDisplay !== 'table' &&
|
||||
<MythicStyledTooltip title={"Table View"}>
|
||||
<IconButton onClick={() =>setTopDisplay("table")}>
|
||||
<TocIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
{topDisplay !== 'graph' &&
|
||||
<MythicStyledTooltip title={"Graph View"} >
|
||||
<IconButton onClick={() =>setTopDisplay("graph")}>
|
||||
<AssessmentIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
{openCallbackImport &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openCallbackImport}
|
||||
onClose={()=>{setOpenCallbackImport(false);}}
|
||||
innerDialog={<ImportCallbackConfigDialog onClose={()=>{setOpenCallbackImport(false);}} />}
|
||||
/>
|
||||
}
|
||||
<MythicStyledTooltip title={"Import previously exported Callbacks"} >
|
||||
<IconButton onClick={() =>setOpenCallbackImport(true)}>
|
||||
<PhoneForwardedIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</Paper>
|
||||
<CallbacksTop
|
||||
callbackTableGridRef={callbackTableGridRef}
|
||||
topDisplay={topDisplay}
|
||||
onOpenTab={onOpenTab.current}
|
||||
onOpenTabs={onOpenTabs.current}
|
||||
me={me} clickedTabId={clickedTabId}/>
|
||||
</div>
|
||||
<div className="bg-gray-mid">
|
||||
<div >
|
||||
<CallbacksTabs
|
||||
onCloseTab={onCloseTab}
|
||||
onEditTabDescription={onEditTabDescription}
|
||||
key={'callbackstabs'}
|
||||
clickedTabId={clickedTabId}
|
||||
setClickedTabId={setClickedTabId}
|
||||
openTabs={openTabs}
|
||||
onDragTab={onDragTab}
|
||||
me={me}
|
||||
onDragEnd={onDragEnd}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
newDataForTab={newDataForTab}
|
||||
setNewDataForTab={setNewDataForTab}
|
||||
/>
|
||||
</div>
|
||||
</Split>
|
||||
</>
|
||||
);
|
||||
}
|
||||
/*
|
||||
<div style={{ maxWidth: '100%', height: '100%', flexDirection: 'column'}}>
|
||||
|
||||
<React.Fragment>
|
||||
<SpeedDialWrapper setTopDisplay={setTopDisplay} heights={heights} onSubmitHeights={onSubmitHeights} />
|
||||
<div style={{flexGrow: 1, flexBasis: heights.top, height: heights.top }}>
|
||||
<CallbacksTop topDisplay={topDisplay} onOpenTab={onOpenTab.current} heights={heights} me={me}/>
|
||||
</div>
|
||||
<div style={{ flexGrow: 1, flexBasis: heights.bottom, height: heights.bottom }}>
|
||||
<CallbacksTabs
|
||||
onCloseTab={onCloseTab}
|
||||
onEditTabDescription={onEditTabDescription}
|
||||
tabHeight={heights.bottom}
|
||||
maxHeight={heights.bottom}
|
||||
key={'callbackstabs'}
|
||||
clickedTabId={clickedTabId}
|
||||
openTabs={openTabs}
|
||||
onDragTab={onDragTab}
|
||||
me={me}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
/>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
</div>
|
||||
*/
|
||||
function SpeedDialWrapperPreMemo({ setTopDisplay }) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [openCallbackImport, setOpenCallbackImport] = React.useState(false);
|
||||
const actions = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
icon: <TocIcon />,
|
||||
name: 'Table layout',
|
||||
onClick: () => {
|
||||
setTopDisplay('table');
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <AssessmentIcon />,
|
||||
name: 'Graph layout',
|
||||
onClick: () => {
|
||||
setTopDisplay('graph');
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <PhoneForwardedIcon />,
|
||||
name: "Import Callback",
|
||||
onClick: () => {
|
||||
setOpenCallbackImport(true);
|
||||
}
|
||||
}
|
||||
],
|
||||
[] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
);
|
||||
return (
|
||||
<React.Fragment>
|
||||
{openCallbackImport &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openCallbackImport}
|
||||
onClose={()=>{setOpenCallbackImport(false);}}
|
||||
innerDialog={<ImportCallbackConfigDialog onClose={()=>{setOpenCallbackImport(false);}} />}
|
||||
/>
|
||||
}
|
||||
<StyledSpeedDial
|
||||
ariaLabel='SpeedDial example'
|
||||
className={classes.speedDial}
|
||||
icon={<SpeedDialIcon />}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
onOpen={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
FabProps={{ color: 'info', size: "small", variant: "extended" }}
|
||||
open={open}
|
||||
style={{ marginTop:"35px" }}
|
||||
direction='down'>
|
||||
{actions.map((action) => (
|
||||
<SpeedDialAction
|
||||
arrow
|
||||
className={classes.speedDialAction}
|
||||
key={action.name}
|
||||
TooltipClasses={{ ".MuiTooltip-tooltip": classes.tooltip,
|
||||
".MuiTooltip-tooltipArrow": classes.arrow,
|
||||
|
||||
}}
|
||||
icon={action.icon}
|
||||
tooltipTitle={action.name}
|
||||
onClick={action.onClick}
|
||||
/>
|
||||
))}
|
||||
</StyledSpeedDial>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
const SpeedDialWrapper = React.memo(SpeedDialWrapperPreMemo);
|
||||
}
|
||||
@@ -1,16 +1,11 @@
|
||||
import React, {useRef, useEffect, useState, useMemo, useContext} from 'react';
|
||||
import React, {useEffect, useState, useMemo, useContext} from 'react';
|
||||
import {DrawC2PathElementsFlowWithProvider} from './C2PathDialog';
|
||||
import {Button} from '@mui/material';
|
||||
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Grow from '@mui/material/Grow';
|
||||
import Popper from '@mui/material/Popper';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import MenuList from '@mui/material/MenuList';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import {useMutation } from '@apollo/client';
|
||||
import {saveSvgAsPng} from 'save-svg-as-png';
|
||||
import {hideCallbackMutation, removeEdgeMutation, addEdgeMutation} from './CallbackMutations';
|
||||
import { MythicDialog } from '../../MythicComponents/MythicDialog';
|
||||
import {MythicSelectFromListDialog} from '../../MythicComponents/MythicSelectFromListDialog';
|
||||
@@ -25,6 +20,9 @@ import InputLabel from '@mui/material/InputLabel';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import Select from '@mui/material/Select';
|
||||
import {CallbackGraphEdgesContext, CallbacksContext, OnOpenTabContext} from './CallbacksTop';
|
||||
import {Dropdown, DropdownMenuItem} from "../../MythicComponents/MythicNestedMenus";
|
||||
import {GetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {operatorSettingDefaults} from "../../../cache";
|
||||
|
||||
const ITEM_HEIGHT = 48;
|
||||
const ITEM_PADDING_TOP = 1;
|
||||
@@ -91,8 +89,8 @@ const GraphViewOptions = ({viewConfig, setViewConfig}) => {
|
||||
evt.stopPropagation();
|
||||
setDropdownOpen((prevOpen) => !prevOpen);
|
||||
};
|
||||
const handleMenuItemClick = (event, index) => {
|
||||
options[index].click();
|
||||
const handleMenuItemClick = (event, click) => {
|
||||
click();
|
||||
setDropdownOpen(false);
|
||||
};
|
||||
const options = [
|
||||
@@ -129,7 +127,7 @@ const GraphViewOptions = ({viewConfig, setViewConfig}) => {
|
||||
};
|
||||
return (
|
||||
<div >
|
||||
<ButtonGroup variant="contained" ref={dropdownAnchorRef} aria-label="split button" style={{marginTop: "10px"}} color="primary">
|
||||
<ButtonGroup variant="contained" ref={dropdownAnchorRef} aria-label="split button" style={{marginTop: "0px", marginLeft: "25px"}} color="primary">
|
||||
<Button size="small" color="primary" aria-controls={dropdownOpen ? 'split-button-menu' : undefined}
|
||||
aria-expanded={dropdownOpen ? 'true' : undefined}
|
||||
aria-haspopup="menu"
|
||||
@@ -138,7 +136,7 @@ const GraphViewOptions = ({viewConfig, setViewConfig}) => {
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
{showConfiguration &&
|
||||
<FormControl sx={{ width: 200, marginTop: "10px", backgroundColor: theme.palette.background.default}} >
|
||||
<FormControl sx={{ width: 200, backgroundColor: theme.palette.background.paper}} >
|
||||
<InputLabel id="demo-chip-label">Group Callbacks By</InputLabel>
|
||||
<Select
|
||||
labelId="demo-chip-label"
|
||||
@@ -159,7 +157,7 @@ const GraphViewOptions = ({viewConfig, setViewConfig}) => {
|
||||
</FormControl>
|
||||
}
|
||||
{showConfiguration &&
|
||||
<FormControl sx={{minWidth: 300, marginTop: "10px",backgroundColor: theme.palette.background.default}}>
|
||||
<FormControl sx={{minWidth: 300, backgroundColor: theme.palette.background.paper}}>
|
||||
<InputLabel id="demo-multiple-chip-label">Display Properties per Callback</InputLabel>
|
||||
<Select
|
||||
labelId="demo-multiple-chip-label"
|
||||
@@ -183,40 +181,33 @@ const GraphViewOptions = ({viewConfig, setViewConfig}) => {
|
||||
</FormControl>
|
||||
}
|
||||
|
||||
<Popper open={dropdownOpen} anchorEl={dropdownAnchorRef.current} transition role={undefined} style={{zIndex: 200}}>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
|
||||
}}
|
||||
>
|
||||
<Paper style={{backgroundColor: theme.palette.mode === 'dark' ? theme.palette.primary.dark : theme.palette.primary.light, color: "white"}}>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<MenuList id="split-button-menu">
|
||||
{options.map((option, index) => (
|
||||
<MenuItem
|
||||
key={option.name}
|
||||
onClick={(event) => handleMenuItemClick(event, index)}
|
||||
>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<Dropdown
|
||||
isOpen={dropdownAnchorRef.current}
|
||||
onOpen={setDropdownOpen}
|
||||
externallyOpen={dropdownOpen}
|
||||
menu={
|
||||
options.map((option, index) => (
|
||||
<DropdownMenuItem
|
||||
key={option.name}
|
||||
disabled={option.disabled}
|
||||
onClick={(event) => handleMenuItemClick(event, option.click)}
|
||||
>
|
||||
{option.name}
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
}
|
||||
/>
|
||||
</ClickAwayListener>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export function CallbacksGraph({}){
|
||||
const theme = useTheme();
|
||||
export function CallbacksGraph({onOpenTab}){
|
||||
const callbacks = useContext(CallbacksContext);
|
||||
const callbackgraphedges = useContext(CallbackGraphEdgesContext);
|
||||
const onOpenTab = useContext(OnOpenTabContext);
|
||||
//used for creating a task to do a link command
|
||||
const [loadingSettings, setLoadingSettings] = React.useState(true);
|
||||
const [filterOptions, setFilterOptions] = React.useState({});
|
||||
const [linkCommands, setLinkCommands] = React.useState([]);
|
||||
const [openParametersDialog, setOpenParametersDialog] = React.useState(false);
|
||||
const [openSelectLinkCommandDialog, setOpenSelectLinkCommandDialog] = React.useState(false);
|
||||
@@ -331,7 +322,6 @@ export function CallbacksGraph({}){
|
||||
{
|
||||
title: 'Interact',
|
||||
onClick: function(node){
|
||||
console.log(node);
|
||||
onOpenTab({tabType: "interact", tabID: node.callback_id + "interact", callbackID: node.callback_id});
|
||||
}
|
||||
},
|
||||
@@ -376,14 +366,40 @@ export function CallbacksGraph({}){
|
||||
}
|
||||
},
|
||||
]}, [getLinkCommands, hideCallback, viewConfig, onOpenTab]);
|
||||
|
||||
React.useEffect( () => {
|
||||
try {
|
||||
const storageItemOptions = GetMythicSetting({setting_name: "callbacks_table_filter_options", default_value: operatorSettingDefaults.callbacks_table_filters});
|
||||
if(storageItemOptions !== null){
|
||||
setFilterOptions(storageItemOptions);
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load callbacks_table_filter_options", error);
|
||||
}
|
||||
setLoadingSettings(false);
|
||||
}, []);
|
||||
if(loadingSettings){
|
||||
return (<div style={{width: '100%', height: '100%', position: "relative",}}>
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%"
|
||||
}}>
|
||||
{"Loading Callbacks and Connections..."}
|
||||
</div>
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
return (
|
||||
<div style={{maxWidth: "100%", "overflow": "hidden", height: "100%"}}>
|
||||
|
||||
{manuallyRemoveEdgeDialogOpen &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={manuallyRemoveEdgeDialogOpen}
|
||||
onClose={()=>{setManuallyRemoveEdgeDialogOpen(false);}}
|
||||
innerDialog={<MythicSelectFromListDialog onClose={()=>{setManuallyRemoveEdgeDialogOpen(false);}} identifier="id" display="display"
|
||||
onClose={() => {
|
||||
setManuallyRemoveEdgeDialogOpen(false);
|
||||
}}
|
||||
innerDialog={<MythicSelectFromListDialog onClose={() => {
|
||||
setManuallyRemoveEdgeDialogOpen(false);}} identifier="id" display="display"
|
||||
onSubmit={onSubmitManuallyRemoveEdge} options={edgeOptions} title={"Manually Remove Edge"} action={"remove"} />}
|
||||
/>
|
||||
}
|
||||
@@ -408,8 +424,9 @@ export function CallbacksGraph({}){
|
||||
action={"select"} display={"display"} identifier={"display"}/>}
|
||||
/>
|
||||
}
|
||||
<DrawC2PathElementsFlowWithProvider providedNodes={callbacks} edges={callbackgraphedges} view_config={viewConfig} theme={theme}
|
||||
panel={<GraphViewOptions setViewConfig={setViewConfig} viewConfig={viewConfig} />} contextMenu={contextMenu}/>
|
||||
<DrawC2PathElementsFlowWithProvider providedNodes={callbacks} edges={callbackgraphedges} view_config={viewConfig}
|
||||
filterOptions={filterOptions}
|
||||
panel={<GraphViewOptions setViewConfig={setViewConfig} viewConfig={viewConfig} />} contextMenu={contextMenu}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,118 @@
|
||||
import React from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import {reorder} from "../../MythicComponents/MythicDraggableList";
|
||||
import {
|
||||
Draggable,
|
||||
DragDropContext,
|
||||
Droppable,
|
||||
} from "@hello-pangea/dnd";
|
||||
import { List, ListItem, ListItemText, Paper } from '@mui/material';
|
||||
import DragHandleIcon from '@mui/icons-material/DragHandle';
|
||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
|
||||
export function CallbacksTableColumnsReorderDialog({initialItems, onSubmit, onClose, onReset, visible, hidden}) {
|
||||
const [items, setItems] = React.useState(initialItems);
|
||||
const theme = useTheme();
|
||||
const onDragEnd = ({ destination, source }) => {
|
||||
// dropped outside the list
|
||||
if (!destination) return;
|
||||
const newItems = reorder(items, source.index, destination.index);
|
||||
setItems(newItems);
|
||||
};
|
||||
React.useEffect( () => {
|
||||
const newItems = items.map( c => {
|
||||
if(visible.includes(c.name)){
|
||||
return {...c, visible: true};
|
||||
}
|
||||
return {...c, visible: false};
|
||||
})
|
||||
setItems(newItems);
|
||||
}, [visible, hidden]);
|
||||
const onToggleVisibility = (i) => {
|
||||
const newItems = items.map( (c, index) => {
|
||||
if(index === i){
|
||||
return {...c, visible: !c.visible};
|
||||
}
|
||||
return {...c};
|
||||
});
|
||||
setItems(newItems);
|
||||
}
|
||||
const onFinish = () => {
|
||||
onSubmit(items);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Drag and Drop to Adjust the Order and Toggle Visibility</DialogTitle>
|
||||
<DialogContent dividers={true} style={{height: "100%", margin: 0, padding: 0, background: theme.palette.background.main}}>
|
||||
<DraggableList items={items} onToggleVisibility={onToggleVisibility} onDragEnd={onDragEnd} />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant="contained" color="primary">
|
||||
Close
|
||||
</Button>
|
||||
<Button onClick={onReset} variant="contained" color="warning">
|
||||
Reset
|
||||
</Button>
|
||||
<Button onClick={onFinish} variant="contained" color="success">
|
||||
Submit
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export const DraggableList = ({ items, onDragEnd, onToggleVisibility }) => {
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="callback-table-column-list">
|
||||
{(provided) => (
|
||||
<List style={{border: 0, padding: 0}} ref={provided.innerRef} {...provided.droppableProps}>
|
||||
{items.map((item, index) => (
|
||||
<DraggableListItem onToggleVisibility={onToggleVisibility} item={item} index={index} key={item.key} />
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</List>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
);
|
||||
};
|
||||
export const DraggableListItem = ({ item, index, onToggleVisibility }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Draggable draggableId={item.key} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<ListItem
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
component={Paper}
|
||||
disabled={!item.visible}
|
||||
sx={snapshot.isDragging ? { background: theme.palette.secondary.main } : {
|
||||
|
||||
}}
|
||||
>
|
||||
<DragHandleIcon style={{marginRight: "10px"}}/>
|
||||
<ListItemText primary={item.name} />
|
||||
<IconButton onClick={() => onToggleVisibility(index)} style={{float: "right", margin: 0, padding: 0}}>
|
||||
{item.visible ? (
|
||||
<VisibilityIcon color={"success"} />
|
||||
) : (
|
||||
<VisibilityOffIcon color={"error"} />
|
||||
)
|
||||
|
||||
}
|
||||
</IconButton>
|
||||
</ListItem>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
};
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import Box from '@mui/material/Box';
|
||||
import AceEditor from 'react-ace';
|
||||
import "ace-builds/src-noconflict/mode-json";
|
||||
import "ace-builds/src-noconflict/theme-github";
|
||||
import "ace-builds/src-noconflict/theme-monokai";
|
||||
import "ace-builds/src-noconflict/ext-searchbox";
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import {HexColorInput, HexColorPicker} from 'react-colorful';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
export function CallbacksTableEditDescriptionColorDialog(props) {
|
||||
const [comment, setComment] = React.useState("");
|
||||
const [color, setColor] = React.useState("");
|
||||
const theme = useTheme();
|
||||
const onCommitSubmit = () => {
|
||||
if(color === "ffffff" || color === "000000"){
|
||||
props.onSubmit(comment, "");
|
||||
} else {
|
||||
props.onSubmit(comment, color);
|
||||
}
|
||||
if(props.dontCloseOnSubmit){
|
||||
return;
|
||||
}
|
||||
props.onClose();
|
||||
}
|
||||
const onChange = (value) => {
|
||||
setComment(value);
|
||||
}
|
||||
useEffect( () => {
|
||||
try{
|
||||
setComment(JSON.stringify(JSON.parse(props.description), null, 2));
|
||||
}catch(error){
|
||||
setComment(props.description);
|
||||
}
|
||||
setColor(props.color);
|
||||
}, [props.description, props.color]);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">{props.title}</DialogTitle>
|
||||
<DialogContent dividers={true} style={{height: "100%", margin: 0, padding: 0}}>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
|
||||
width="100%"
|
||||
height="100%"
|
||||
fontSize={14}
|
||||
wrapEnabled={props.wrap ? props.wrap : false}
|
||||
minLines={props.maxRows ? props.maxRows : 3}
|
||||
maxLines={props.maxRows ? props.maxRows : 3}
|
||||
value={comment}
|
||||
focus={true}
|
||||
onChange={onChange}
|
||||
setOptions={{
|
||||
tabSize: 4,
|
||||
useWorker: false,
|
||||
showInvisibles: false,
|
||||
}}
|
||||
/>
|
||||
<HexColorPicker color={color} onChange={setColor} style={{width: "100%"}} />
|
||||
<HexColorInput color={color} onChange={setColor} style={{width: "80%"}} />
|
||||
<Button onClick={()=>setColor("ffffff")} color={"success"}>
|
||||
Clear
|
||||
</Button>
|
||||
<Box sx={{width: "100%", height: 25, backgroundColor: color}} >
|
||||
<Typography style={{color: "white", display: "inline-block", marginRight: "10px"}}>DarkMode Text With Color Background</Typography>
|
||||
<Typography style={{color: "black", display: "inline-block"}}>LightMode Text With Color Background</Typography>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
Close
|
||||
</Button>
|
||||
{props.onSubmit &&
|
||||
<Button onClick={onCommitSubmit} variant="contained" color="success">
|
||||
Submit
|
||||
</Button>
|
||||
}
|
||||
</DialogActions>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import Box from '@mui/material/Box';
|
||||
import AceEditor from 'react-ace';
|
||||
import "ace-builds/src-noconflict/mode-json";
|
||||
import "ace-builds/src-noconflict/theme-github";
|
||||
import "ace-builds/src-noconflict/theme-monokai";
|
||||
import "ace-builds/src-noconflict/ext-searchbox";
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import {HexColorInput, HexColorPicker} from 'react-colorful';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import MythicTextField from "../../MythicComponents/MythicTextField";
|
||||
|
||||
export function CallbacksTableEditTriggerOnCheckinDialog(props) {
|
||||
const [comment, setComment] = React.useState(0);
|
||||
const onChange = (name, value, error) => {
|
||||
if(isNaN(parseInt(value))){
|
||||
setComment(0);
|
||||
} else {
|
||||
setComment(parseInt(value));
|
||||
}
|
||||
}
|
||||
useEffect( () => {
|
||||
setComment(props.trigger_on_checkin_after_time);
|
||||
}, [props.trigger_on_checkin_after_time]);
|
||||
const onSubmit = () => {
|
||||
props.onSubmit(comment);
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">{"Adjust this callback's trigger threshold"}</DialogTitle>
|
||||
<DialogContent dividers={true} style={{height: "100%"}}>
|
||||
<Typography >
|
||||
This adjusts how long, in minutes, this callback must <b>not</b> checkin before finally checking in to trigger an <b>eventing workflow</b> (trigger is callback_checkin).
|
||||
</Typography>
|
||||
<Typography >
|
||||
A zero value means never trigger.
|
||||
</Typography>
|
||||
<Typography >
|
||||
If no eventing workflow for <b>callback_checkin</b> that matches the right payload_types and supported_os restrictions, then nothing will happen.
|
||||
</Typography>
|
||||
<MythicTextField autoFocus={true} onChange={onChange} type={"number"} value={comment} onEnter={onSubmit} name={"trigger threshold in minutes"} showLabel={false} />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
Close
|
||||
</Button>
|
||||
{props.onSubmit &&
|
||||
<Button onClick={onSubmit} variant="contained" color="success">
|
||||
Submit
|
||||
</Button>
|
||||
}
|
||||
</DialogActions>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, {useCallback, useEffect, useContext} from 'react';
|
||||
import {Button} from '@mui/material';
|
||||
import {IconButton} from '@mui/material';
|
||||
import { MythicDialog } from '../../MythicComponents/MythicDialog';
|
||||
import { MythicDisplayTextDialog} from '../../MythicComponents/MythicDisplayTextDialog';
|
||||
import {MythicModifyStringDialog} from '../../MythicComponents/MythicDialog';
|
||||
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||
import KeyboardIcon from '@mui/icons-material/Keyboard';
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
@@ -11,55 +10,34 @@ import WifiIcon from '@mui/icons-material/Wifi';
|
||||
import InsertLinkTwoToneIcon from '@mui/icons-material/InsertLinkTwoTone';
|
||||
import {C2PathDialog} from './C2PathDialog';
|
||||
import {snackActions} from '../../utilities/Snackbar';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Grow from '@mui/material/Grow';
|
||||
import Popper from '@mui/material/Popper';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import MenuList from '@mui/material/MenuList';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
import {
|
||||
hideCallbackMutation,
|
||||
lockCallbackMutation,
|
||||
unlockCallbackMutation,
|
||||
updateIPsCallbackMutation,
|
||||
exportCallbackConfigQuery
|
||||
} from './CallbackMutations';
|
||||
import {useMutation, useLazyQuery } from '@apollo/client';
|
||||
import {useMutation } from '@apollo/client';
|
||||
import SnoozeIcon from '@mui/icons-material/Snooze';
|
||||
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import {faQuestion, faSkullCrossbones, faFolderOpen, faList, faRobot} from '@fortawesome/free-solid-svg-icons';
|
||||
import {faQuestion, faSkullCrossbones, faRobot} from '@fortawesome/free-solid-svg-icons';
|
||||
import {faLinux, faApple, faWindows, faChrome, faAndroid} from '@fortawesome/free-brands-svg-icons';
|
||||
import {DetailedCallbackTable} from './DetailedCallbackTable';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip';
|
||||
import {TaskFromUIButton} from './TaskFromUIButton';
|
||||
import { MythicSelectFromRawListDialog } from '../../MythicComponents/MythicSelectFromListDialog';
|
||||
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
|
||||
import { areEqual } from 'react-window';
|
||||
import {CallbackGraphEdgesContext, OnOpenTabContext} from './CallbacksTop';
|
||||
import Moment from 'react-moment';
|
||||
import moment from 'moment';
|
||||
import WidgetsIcon from '@mui/icons-material/Widgets';
|
||||
import {ModifyCallbackMythicTreeGroupsDialog} from "./ModifyCallbackMythicTreeGroupsDialog";
|
||||
import {GetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import TerminalIcon from '@mui/icons-material/Terminal';
|
||||
import ImportExportIcon from '@mui/icons-material/ImportExport';
|
||||
import VerticalSplitIcon from '@mui/icons-material/VerticalSplit';
|
||||
import NotificationsActiveTwoToneIcon from '@mui/icons-material/NotificationsActiveTwoTone';
|
||||
import {faSocks} from '@fortawesome/free-solid-svg-icons';
|
||||
import {TagsDisplay, ViewEditTags} from "../../MythicComponents/MythicTag";
|
||||
|
||||
export const CallbacksTableIDCell = React.memo(({rowData, toggleLock, updateDescription, setOpenHideMultipleDialog, setOpenTaskMultipleDialog}) =>{
|
||||
const dropdownAnchorRef = React.useRef(null);
|
||||
export const CallbacksTableIDCell = React.memo(({rowData, callbackDropdown}) =>{
|
||||
const theme = useTheme();
|
||||
const dropdownAnchorRef = React.useRef(null);
|
||||
const onOpenTab = useContext(OnOpenTabContext);
|
||||
const [openMetaDialog, setOpenMetaDialog] = React.useState(false);
|
||||
const [dropdownOpen, setDropdownOpen] = React.useState(false);
|
||||
const [openEditDescriptionDialog, setOpenEditDescriptionDialog] = React.useState(false);
|
||||
const [openTaskingButton, setOpenTaskingButton] = React.useState(false);
|
||||
const [openEditMythicTreeGroupsDialog, setOpenEditMythicTreeGroupsDialog] = React.useState(false);
|
||||
const taskingData = React.useRef({"parameters": "", "ui_feature": "callback_table:exit"});
|
||||
const interactType = GetMythicSetting({setting_name: "interactType", default_value: "interact"})
|
||||
const [rowDataStatic, setRowDataStatic] = React.useState(rowData);
|
||||
React.useEffect( () => {
|
||||
let update = false;
|
||||
@@ -81,272 +59,84 @@ export const CallbacksTableIDCell = React.memo(({rowData, toggleLock, updateDesc
|
||||
if(rowData.id !== rowDataStatic.id){
|
||||
update = true;
|
||||
}
|
||||
if(rowData.color !== rowDataStatic.color){
|
||||
update = true;
|
||||
}
|
||||
if(rowData.trigger_on_checkin_after_time !== rowDataStatic.trigger_on_checkin_after_time){
|
||||
update = true;
|
||||
}
|
||||
if(rowData.callbackports.length !== rowDataStatic.callbackports.length){
|
||||
update = true;
|
||||
}
|
||||
if(update){
|
||||
setRowDataStatic(rowData);
|
||||
}
|
||||
}, [rowData]);
|
||||
const [lockCallback] = useMutation(lockCallbackMutation, {
|
||||
update: (cache, {data}) => {
|
||||
if(data.updateCallback.status === "success"){
|
||||
snackActions.success("Locked callback");
|
||||
}else{
|
||||
snackActions.warning(data.updateCallback.error);
|
||||
const proxyMessage = useCallback(() => {
|
||||
let message = [];
|
||||
for(let i = 0; i < rowDataStatic?.callbackports?.length || 0; i++){
|
||||
switch(rowDataStatic.callbackports[i].port_type){
|
||||
case "socks":
|
||||
if(rowDataStatic.callbackports[i].username === ""){
|
||||
message.push(`socks: ${rowDataStatic.callbackports[i].local_port}`);
|
||||
} else {
|
||||
message.push(`socks: ${rowDataStatic.callbackports[i].local_port}, ${rowDataStatic.callbackports[i].username}:${rowDataStatic.callbackports[i].password}`);
|
||||
}
|
||||
break;
|
||||
case "rpfwd":
|
||||
if(rowDataStatic.callbackports[i].username === ""){
|
||||
message.push(`rpfwd: ${rowDataStatic.callbackports[i].local_port}->${rowDataStatic.callbackports[i].remote_ip}:${rowDataStatic.callbackports[i].remote_port}`);
|
||||
} else {
|
||||
message.push(`pfwd: ${rowDataStatic.callbackports[i].local_port}->${rowDataStatic.callbackports[i].remote_ip}:${rowDataStatic.callbackports[i].remote_port}, ${rowDataStatic.callbackports[i].username}:${rowDataStatic.callbackports[i].password}`);
|
||||
}
|
||||
break;
|
||||
case "interactive":
|
||||
message.push(`interactive: ${rowDataStatic.callbackports[i].local_port}`);
|
||||
break;
|
||||
}
|
||||
|
||||
},
|
||||
onError: data => {
|
||||
console.log(data);
|
||||
snackActions.warning(data);
|
||||
}
|
||||
});
|
||||
const [unlockCallback] = useMutation(unlockCallbackMutation, {
|
||||
update: (cache, {data}) => {
|
||||
if(data.updateCallback.status === "success"){
|
||||
snackActions.success("Unlocked callback");
|
||||
}else{
|
||||
snackActions.warning(data.updateCallback.error);
|
||||
}
|
||||
|
||||
},
|
||||
onError: data => {
|
||||
console.log(data);
|
||||
snackActions.warning(data);
|
||||
}
|
||||
});
|
||||
const editDescriptionSubmit = (description) => {
|
||||
if(description === ""){
|
||||
updateDescription({description: rowDataStatic.payload.description, callback_display_id: rowDataStatic.display_id});
|
||||
} else {
|
||||
updateDescription({description, callback_display_id: rowDataStatic.display_id});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
const handleDropdownToggle = (evt) => {
|
||||
evt.stopPropagation();
|
||||
setDropdownOpen((prevOpen) => !prevOpen);
|
||||
};
|
||||
const localOnOpenTab = (tabType) => {
|
||||
if(tabType === "interact"){
|
||||
onOpenTab({tabType, tabID: rowDataStatic.id + tabType, callbackID: rowDataStatic.id, displayID: rowDataStatic.display_id});
|
||||
}else if(tabType === "processBrowser"){
|
||||
onOpenTab({tabType, tabID: rowDataStatic.host, callbackID: rowDataStatic.id, displayID: rowDataStatic.display_id});
|
||||
}else{
|
||||
onOpenTab({tabType, tabID: rowDataStatic.id + tabType, callbackID: rowDataStatic.id, displayID: rowDataStatic.display_id});
|
||||
}
|
||||
}
|
||||
const handleMenuItemClick = (event, index) => {
|
||||
options[index].click(event);
|
||||
setDropdownOpen(false);
|
||||
return message.join("\n");
|
||||
}, [rowDataStatic]);
|
||||
const handleDropdownToggle = (event) => {
|
||||
event.stopPropagation();
|
||||
callbackDropdown({rowDataStatic, event});
|
||||
};
|
||||
const handleClose = (event) => {
|
||||
if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDropdownOpen(false);
|
||||
};
|
||||
const [hideCallback] = useMutation(hideCallbackMutation, {
|
||||
update: (cache, {data}) => {
|
||||
if(data.updateCallback.status === "success"){
|
||||
snackActions.success("Hiding callback");
|
||||
}else{
|
||||
snackActions.warning(data.updateCallback.error);
|
||||
}
|
||||
|
||||
},
|
||||
onError: data => {
|
||||
console.log(data);
|
||||
}
|
||||
});
|
||||
const localToggleLock = () => {
|
||||
if(rowDataStatic.locked){
|
||||
unlockCallback({variables: {callback_display_id: rowDataStatic.display_id}})
|
||||
}else{
|
||||
lockCallback({variables: {callback_display_id: rowDataStatic.display_id}})
|
||||
}
|
||||
const localOnOpenTab = () => {
|
||||
onOpenTab({tabType: interactType, tabID: rowDataStatic.id + interactType, callbackID: rowDataStatic.id, displayID: rowDataStatic.display_id});
|
||||
}
|
||||
const [exportConfig] = useLazyQuery(exportCallbackConfigQuery, {
|
||||
fetchPolicy: "no-cache",
|
||||
onCompleted: (data) => {
|
||||
if(data.exportCallbackConfig.status === "success"){
|
||||
const dataBlob = new Blob([data.exportCallbackConfig.config], {type: 'text/plain'});
|
||||
const ele = document.getElementById("download_config");
|
||||
if(ele !== null){
|
||||
ele.href = URL.createObjectURL(dataBlob);
|
||||
ele.download = rowDataStatic.agent_callback_id + ".json";
|
||||
ele.click();
|
||||
}else{
|
||||
const element = document.createElement("a");
|
||||
element.id = "download_config";
|
||||
element.href = URL.createObjectURL(dataBlob);
|
||||
element.download = rowDataStatic.agent_callback_id + ".json";
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
}
|
||||
}else{
|
||||
snackActions.error("Failed to export configuration: " + data.exportCallbackConfig.error);
|
||||
}
|
||||
},
|
||||
onError: (data) => {
|
||||
console.log(data);
|
||||
snackActions.error("Failed to export configuration: " + data.message)
|
||||
}
|
||||
})
|
||||
const options = [
|
||||
{name: 'Hide Callback', icon: <VisibilityOffIcon style={{color: theme.palette.warning.main, paddingRight: "5px"}}/>, click: (evt) => {
|
||||
evt.stopPropagation();
|
||||
hideCallback({variables: {callback_display_id: rowDataStatic.display_id}});
|
||||
}},
|
||||
{
|
||||
name: "Hide Multiple", icon: <VisibilityOffIcon style={{paddingRight: "5px"}}/>, click: (evt) => {
|
||||
setOpenHideMultipleDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Exit Callback", icon: <FontAwesomeIcon icon={faSkullCrossbones} style={{color: theme.errorOnMain, cursor: "pointer", marginRight: "10px"}} />, click: (evt) => {
|
||||
taskingData.current = {"parameters": "", "ui_feature": "callback_table:exit", "getConfirmation": true, acceptText: "exit"};
|
||||
setOpenTaskingButton(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Task Multiple", icon: <FontAwesomeIcon icon={faList} style={{cursor: "pointer", marginRight: "10px"}} />, click: (evt) => {
|
||||
setOpenTaskMultipleDialog({open: true, data: rowDataStatic});
|
||||
}
|
||||
},
|
||||
{name: 'File Browser', icon: <FontAwesomeIcon icon={faFolderOpen} style={{color: theme.folderColor, cursor: "pointer", marginRight: "10px"}} />, click: (evt) => {
|
||||
evt.stopPropagation();
|
||||
localOnOpenTab("fileBrowser");
|
||||
}},
|
||||
{name: 'Process Browser', icon: <AccountTreeIcon style={{paddingRight: "5px"}}/>, click: (evt) => {
|
||||
evt.stopPropagation();
|
||||
localOnOpenTab("processBrowser");
|
||||
}},
|
||||
{name: 'Split Tasking', icon: <KeyboardIcon style={{paddingRight: "5px"}}/>, click: (evt) => {
|
||||
evt.stopPropagation();
|
||||
localOnOpenTab("interactSplit");
|
||||
}},
|
||||
{name: "Console View", icon: <TerminalIcon style={{paddingRight: "5px"}}/>, click: (evt) => {
|
||||
evt.stopPropagation();
|
||||
localOnOpenTab("interactConsole");
|
||||
}
|
||||
},
|
||||
{name: rowDataStatic.locked ? 'Unlock (Locked by ' + rowDataStatic.locked_operator.username + ')' : 'Lock Callback', icon: rowDataStatic.locked ? (<LockOpenIcon style={{color: theme.successOnMain, paddingRight: "5px"}}/>) : (<LockIcon style={{color: theme.errorOnMain, paddingRight: "5px"}} />), click: (evt) => {
|
||||
evt.stopPropagation();
|
||||
localToggleLock();
|
||||
}},
|
||||
{name: "Edit Description", icon: <EditIcon style={{paddingRight: "5px"}} />, click: (evt) => {
|
||||
evt.stopPropagation();
|
||||
setOpenEditDescriptionDialog(true);
|
||||
}},
|
||||
{name: "Expand Callback", icon: <OpenInNewIcon style={{paddingRight: "5px"}} />, click: (evt) => {
|
||||
evt.stopPropagation();
|
||||
window.open("/new/callbacks/" + rowDataStatic.display_id, "_blank").focus();
|
||||
}},
|
||||
{name: "Export Callback", icon: <ImportExportIcon style={{paddingRight: "5px"}} />, click: (evt) => {
|
||||
evt.stopPropagation();
|
||||
exportConfig({variables: {agent_callback_id: rowDataStatic.agent_callback_id}});
|
||||
}},
|
||||
{name: "View Metadata", icon: <InfoIcon style={{color: theme.infoOnMain, paddingRight: "5px"}} />, click: (evt) => {
|
||||
evt.stopPropagation();
|
||||
setOpenMetaDialog(true);
|
||||
}},
|
||||
{name: "Modify Groupings", icon: <WidgetsIcon style={{paddingRight: "5px"}} />, click: (evt) => {
|
||||
evt.stopPropagation();
|
||||
setOpenEditMythicTreeGroupsDialog(true);
|
||||
}}
|
||||
];
|
||||
let defaultInteractIcon = <KeyboardIcon style={{paddingRight: "5px"}}/>;
|
||||
if(interactType === "interactSplit"){
|
||||
defaultInteractIcon = <VerticalSplitIcon style={{paddingRight: "5px"}}/>;
|
||||
} else if(interactType === "interactConsole"){
|
||||
defaultInteractIcon = <TerminalIcon style={{paddingRight: "5px"}}/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ButtonGroup
|
||||
color={rowDataStatic.integrity_level > 2 ? "error" : "primary"}
|
||||
ref={dropdownAnchorRef}
|
||||
aria-label="split button"
|
||||
<div id={`callbacksTableID${rowDataStatic.id}`} style={{display: "inline-flex", alignItems: "flex-start"}}>
|
||||
<IconButton style={{padding: 0, margin: 0}} color={rowDataStatic.integrity_level > 2 ? "error" : ""}
|
||||
onClick={(evt) => {evt.stopPropagation();localOnOpenTab()}}
|
||||
>
|
||||
<Button style={{padding: "0 10px 0 10px"}} color={rowDataStatic.integrity_level > 2 ? "error" : "primary"} variant="contained"
|
||||
onClick={(evt) => {evt.stopPropagation();localOnOpenTab("interact")}}>
|
||||
{ rowDataStatic.locked ? (<LockIcon fontSize="large" style={{color: theme.errorOnMain, marginRight: "10px"}} />):(<KeyboardIcon fontSize="large" style={{marginRight: "10px"}}/>) }
|
||||
{rowDataStatic.display_id}
|
||||
</Button>
|
||||
<Button
|
||||
style={{margin: 0, padding: 0}}
|
||||
variant="contained"
|
||||
aria-controls={dropdownOpen ? 'split-button-menu' : undefined}
|
||||
aria-expanded={dropdownOpen ? 'true' : undefined}
|
||||
color={rowDataStatic.integrity_level > 2 ? "error" : "primary"}
|
||||
aria-haspopup="menu"
|
||||
onClick={handleDropdownToggle}
|
||||
>
|
||||
<ArrowDropDownIcon/>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Popper open={dropdownOpen} anchorEl={dropdownAnchorRef.current} role={undefined} transition style={{zIndex: 200}}>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin: placement === 'bottom' ? 'top left' : 'top center',
|
||||
}}
|
||||
>
|
||||
<Paper className={"dropdownMenuColored"} elevation={5}>
|
||||
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
|
||||
<MenuList id="split-button-menu">
|
||||
{options.map((option, index) => (
|
||||
<MenuItem
|
||||
key={option.name}
|
||||
onClick={(event) => handleMenuItemClick(event, index)}
|
||||
>
|
||||
{option.icon}{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
{openTaskingButton &&
|
||||
<TaskFromUIButton ui_feature={taskingData.current?.ui_feature || " "}
|
||||
callback_id={rowDataStatic.id}
|
||||
display_id={rowDataStatic.display_id}
|
||||
parameters={taskingData.current?.parameters || ""}
|
||||
openDialog={taskingData.current?.openDialog || false}
|
||||
getConfirmation={taskingData.current?.getConfirmation || false}
|
||||
acceptText={taskingData.current?.acceptText || "YES"}
|
||||
selectCallback={taskingData.current?.selectCallback || false}
|
||||
onTasked={() => setOpenTaskingButton(false)}/>
|
||||
}
|
||||
{openMetaDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="lg" open={openMetaDialog}
|
||||
onClose={()=>{setOpenMetaDialog(false);}}
|
||||
innerDialog={<DetailedCallbackTable onClose={()=>{setOpenMetaDialog(false);}} callback_id={rowDataStatic.id} />}
|
||||
/>
|
||||
{rowDataStatic.locked ? (<LockIcon style={{marginRight: "10px"}} />):(defaultInteractIcon)}
|
||||
</IconButton>
|
||||
{rowDataStatic.display_id}
|
||||
<IconButton
|
||||
style={{margin: 0, padding: 0}}
|
||||
color={rowDataStatic.integrity_level > 2 ? "error" : ""}
|
||||
aria-haspopup="menu"
|
||||
onClick={handleDropdownToggle}
|
||||
ref={dropdownAnchorRef}
|
||||
>
|
||||
<ArrowDropDownIcon/>
|
||||
</IconButton>
|
||||
{rowDataStatic.trigger_on_checkin_after_time > 0 &&
|
||||
<MythicStyledTooltip title={`Alert on callback after no checkin for ${rowDataStatic.trigger_on_checkin_after_time} minutes`}>
|
||||
<NotificationsActiveTwoToneIcon color={"success"}/>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
{openEditDescriptionDialog &&
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
open={openEditDescriptionDialog}
|
||||
onClose={() => {setOpenEditDescriptionDialog(false);}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog title={"Edit Callback's Description"}
|
||||
onClose={() => {setOpenEditDescriptionDialog(false);}}
|
||||
value={rowDataStatic.description}
|
||||
onSubmit={editDescriptionSubmit}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
{openEditMythicTreeGroupsDialog &&
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
maxWidth={"lg"}
|
||||
open={openEditMythicTreeGroupsDialog}
|
||||
onClose={() => {setOpenEditMythicTreeGroupsDialog(false);}}
|
||||
innerDialog={
|
||||
<ModifyCallbackMythicTreeGroupsDialog callback_id={rowDataStatic.id}
|
||||
onClose={() => {setOpenEditMythicTreeGroupsDialog(false);}} />
|
||||
}
|
||||
/>
|
||||
{rowDataStatic.callbackports.length > 0 &&
|
||||
<MythicStyledTooltip title={proxyMessage()}>
|
||||
<FontAwesomeIcon icon={faSocks} size="lg" style={{color: theme.palette.success.main}}/>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
@@ -357,22 +147,42 @@ export const CallbacksTableStringCell = React.memo(({rowData, cellData}) => {
|
||||
<div>{cellData}</div>
|
||||
)
|
||||
}, areEqual)
|
||||
export const CallbacksTableLastCheckinCell = React.memo( ({rowData, cellData}) => {
|
||||
export const CallbacksTableLastCheckinCell = React.memo( ({rowData, cellData, me}) => {
|
||||
const adjustOutput = (newTime) => {
|
||||
if(newTime.includes("54 years")){
|
||||
return "Streaming Now"
|
||||
}else if(newTime === "a few seconds"){
|
||||
if(newTime === "a few seconds"){
|
||||
moment.relativeTimeThreshold('s', 60);
|
||||
moment.relativeTimeThreshold('ss', 0);
|
||||
return moment(rowData.last_checkin + "Z", "YYYY-MM-DDTHH:mm:ss.SSSSSSZ").fromNow(true)
|
||||
return moment(rowData.last_checkin + "Z", "YYYY-MM-DDTHH:mm:ss.SSSSSSZ").subtract(me?.user?.server_skew || 0, 'millisecond').fromNow(true)
|
||||
}
|
||||
return newTime;
|
||||
return moment(rowData.last_checkin + "Z", "YYYY-MM-DDTHH:mm:ss.SSSSSSZ").subtract(me?.user?.server_skew || 0, 'millisecond').fromNow(true);
|
||||
//return newTime;
|
||||
}
|
||||
const theme = useTheme();
|
||||
if(rowData?.payload?.payloadtype?.agent_type !== "agent"){
|
||||
return ""
|
||||
}
|
||||
if(rowData.last_checkin === "1970-01-01T00:00:00"){
|
||||
return (
|
||||
<>
|
||||
{rowData.dead &&
|
||||
<MythicStyledTooltip title={"Based on callback's last checkin and sleep info, it's likely dead"}>
|
||||
<FontAwesomeIcon disabled icon={faSkullCrossbones} style={{
|
||||
color: theme.palette.error.main, cursor: "pointer", marginRight: "10px",}} />
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
{"Streaming Now"}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div style={{display: "flex", alignItems: "center"}}>
|
||||
{rowData.dead &&
|
||||
<MythicStyledTooltip title={"Based on callback's last checkin and sleep info, it's likely dead"}>
|
||||
<FontAwesomeIcon icon={faSkullCrossbones} style={{
|
||||
color: theme.palette.error.main, cursor: "pointer", marginRight: "10px",}} />
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
<Moment filter={adjustOutput} interval={1000} parse={"YYYY-MM-DDTHH:mm:ss.SSSSSSZ"}
|
||||
withTitle
|
||||
titleFormat={"YYYY-MM-DD HH:mm:ss"}
|
||||
@@ -386,12 +196,7 @@ export const CallbacksTableLastCheckinCell = React.memo( ({rowData, cellData}) =
|
||||
}, areEqual);
|
||||
export const CallbacksTablePayloadTypeCell = React.memo( ({rowData}) => {
|
||||
return (
|
||||
<MythicStyledTooltip title={rowData?.payload?.payloadtype?.name}>
|
||||
<img
|
||||
style={{width: "35px", height: "35px"}}
|
||||
src={"/static/" + rowData?.payload?.payloadtype?.name + ".svg"}
|
||||
/>
|
||||
</MythicStyledTooltip>
|
||||
rowData?.payload?.payloadtype?.name
|
||||
)
|
||||
}, areEqual)
|
||||
export const CallbacksTableIPCell = React.memo(({cellData, rowData}) => {
|
||||
@@ -435,16 +240,16 @@ export const CallbacksTableIPCell = React.memo(({cellData, rowData}) => {
|
||||
}, [cellData]);
|
||||
return (
|
||||
<>
|
||||
<div style={{display: "flex", alignItems: "center"}}>
|
||||
<div style={{display: "inline-flex", alignItems: "center", height: "100%"}}>
|
||||
{options.length > 1 &&
|
||||
<MythicStyledTooltip title={"Adjust Displayed"}>
|
||||
<UnfoldMoreIcon onClick={onClick} style={{paddingTop: "5px", cursor: "pointer"}} />
|
||||
<UnfoldMoreIcon onClick={onClick} style={{paddingTop: "5px", cursor: "pointer", width: "unset"}} />
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
{displayIP}
|
||||
</div>
|
||||
{openPickIP &&
|
||||
<MythicDialog fullWidth={true} open={openPickIP} onClose={() => {setOpenPickIP(false);}}
|
||||
<MythicDialog fullWidth={true} open={openPickIP} onClose={() => {setOpenPickIP(false);}}
|
||||
innerDialog={
|
||||
<MythicSelectFromRawListDialog
|
||||
onClose={() => {setOpenPickIP(false);}}
|
||||
@@ -505,10 +310,13 @@ export const CallbacksTableC2Cell = React.memo(({rowData}) => {
|
||||
//look at all of the edges in myEdges and see if there are any edges that share a source/destination in props.callbackgraphedges that are _not_ in myEdges so far
|
||||
const newEdges = initialCallbackGraphEdges?.reduce( (prev, edge) => {
|
||||
//looking to see if we should add 'edge' to our list of relevant edges
|
||||
if(prev.includes(edge)){return [...prev]}
|
||||
const alreadyIncludes = prev.filter( (e) => e.id === edge.id);
|
||||
if(alreadyIncludes.length > 0){
|
||||
return [...prev]
|
||||
}
|
||||
//look through all of the previous edges we know about and see if there's a matching source/destination id with the new edge
|
||||
const matching = prev.filter( (e) => {
|
||||
if(e.source.id === edge.source.id || e.source.id === edge.destination.id || e.destination.id === edge.source.id ){
|
||||
if(e.source.id === edge.source.id || e.source.id === edge.destination.id || e.destination.id === edge.source.id || e.destination.id === edge.destination.id ){
|
||||
if(activeOnly){
|
||||
if(edge.end_timestamp === null) { return true}
|
||||
else{return false}
|
||||
@@ -518,12 +326,12 @@ export const CallbacksTableC2Cell = React.memo(({rowData}) => {
|
||||
return false;
|
||||
});
|
||||
if(matching.length > 0){
|
||||
return [...prev, edge];
|
||||
return [...prev, {...edge}];
|
||||
}else{
|
||||
return [...prev];
|
||||
}
|
||||
}, [...myEdges]) || [];
|
||||
foundMore = newEdges.length > myEdges;
|
||||
foundMore = newEdges.length > myEdges.length;
|
||||
myEdges = [...newEdges];
|
||||
}
|
||||
return myEdges;
|
||||
@@ -536,6 +344,7 @@ export const CallbacksTableC2Cell = React.memo(({rowData}) => {
|
||||
}, [initialCallbackGraphEdges, localRowData]);
|
||||
useEffect( () => {
|
||||
//determine if there are any active routes left at all
|
||||
//console.log(localRowData.display_id, callbackgraphedges)
|
||||
const activeRoutes = callbackgraphedges.filter( (edge) => {
|
||||
if(!edge.c2profile.is_p2p && edge.end_timestamp === null){
|
||||
return true;
|
||||
@@ -588,26 +397,26 @@ export const CallbacksTableOSCell = React.memo( ({rowData, cellData}) => {
|
||||
const [openOSDialog, setOpenOSDialog] = React.useState(false);
|
||||
const getOSIcon = useCallback( () => {
|
||||
if(rowData?.payload?.payloadtype?.agent_type !== "agent"){
|
||||
return <FontAwesomeIcon icon={faRobot} size="2x" style={{cursor: "pointer"}} onClick={displayOSInfo} />
|
||||
return <FontAwesomeIcon icon={faRobot} style={{cursor: "pointer"}} onClick={displayOSInfo} />
|
||||
}
|
||||
switch(rowData.payload.os.toLowerCase()){
|
||||
case "windows":
|
||||
return <FontAwesomeIcon icon={faWindows} size="2x" style={{cursor: "pointer"}} onClick={displayOSInfo} />
|
||||
return <FontAwesomeIcon icon={faWindows} style={{cursor: "pointer"}} onClick={displayOSInfo} />
|
||||
case "linux":
|
||||
case "centos":
|
||||
case "redhat":
|
||||
case "debian":
|
||||
case "fedora":
|
||||
case "freebsd":
|
||||
return <FontAwesomeIcon icon={faLinux} size="2x" style={{cursor: "pointer"}} onClick={displayOSInfo} />
|
||||
return <FontAwesomeIcon icon={faLinux} style={{cursor: "pointer"}} onClick={displayOSInfo} />
|
||||
case "macos":
|
||||
return <FontAwesomeIcon icon={faApple} size="2x" style={{cursor: "pointer"}} onClick={displayOSInfo}/>
|
||||
return <FontAwesomeIcon icon={faApple} style={{cursor: "pointer"}} onClick={displayOSInfo}/>
|
||||
case "chrome":
|
||||
return <FontAwesomeIcon icon={faChrome} size="2x" style={{cursor: "pointer"}} onClick={displayOSInfo} />
|
||||
return <FontAwesomeIcon icon={faChrome} style={{cursor: "pointer"}} onClick={displayOSInfo} />
|
||||
case "android":
|
||||
return <FontAwesomeIcon icon={faAndroid} size="2x" style={{cursor: "pointer"}} onClick={displayOSInfo} />
|
||||
return <FontAwesomeIcon icon={faAndroid} style={{cursor: "pointer"}} onClick={displayOSInfo} />
|
||||
default:
|
||||
return <FontAwesomeIcon icon={faQuestion} size="2x" style={{cursor: "pointer"}} onClick={displayOSInfo} />
|
||||
return <FontAwesomeIcon icon={faQuestion} style={{cursor: "pointer"}} onClick={displayOSInfo} />
|
||||
}
|
||||
}, [rowData?.payload?.os]);
|
||||
const displayOSInfo = React.useCallback( () => {
|
||||
@@ -645,15 +454,18 @@ export const CallbacksTableSleepCell = React.memo( ({rowData, cellData, updateSl
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div style={{height: "100%", display: "flex", alignItems: "center"}}>
|
||||
<SnoozeIcon onClick={onOpenSleepDialog}
|
||||
style={{color: cellData === "" ? theme.palette.warning.main : theme.palette.info.main, cursor: "pointer"}}
|
||||
/>
|
||||
{ openSleepDialog &&
|
||||
<MythicDialog fullWidth={true} open={openSleepDialog} onClose={() => {setOpenSleepDialog(false);}}
|
||||
<MythicDialog fullWidth={true} open={openSleepDialog} maxWidth={"md"}
|
||||
style={{height: "100%"}}
|
||||
onClose={() => {setOpenSleepDialog(false);}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog title={"View Sleep Information"} multiline maxRows={10}
|
||||
onClose={() => {setOpenSleepDialog(false);}}
|
||||
<MythicModifyStringDialog title={"View / Edit Sleep Information - This doesn't issue tasks to the agent, but helps alive/dead tracking"}
|
||||
maxRows={20}
|
||||
onClose={() => {setOpenSleepDialog(false);}}
|
||||
value={cellData}
|
||||
onSubmit={editSleepSubmit} />
|
||||
}
|
||||
@@ -662,5 +474,13 @@ export const CallbacksTableSleepCell = React.memo( ({rowData, cellData, updateSl
|
||||
</div>
|
||||
|
||||
|
||||
)
|
||||
}, areEqual)
|
||||
export const CallbacksTableTagsCell = React.memo(({rowData, cellData}) => {
|
||||
return (
|
||||
<div>
|
||||
<ViewEditTags target_object={"callback_id"} target_object_id={rowData.id} />
|
||||
<TagsDisplay tags={cellData} expand={false} />
|
||||
</div>
|
||||
)
|
||||
}, areEqual)
|
||||
@@ -1,20 +1,30 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import {
|
||||
DragDropContext,
|
||||
Droppable,
|
||||
} from "@hello-pangea/dnd";
|
||||
import { CallbacksTabsTaskingLabel, CallbacksTabsTaskingPanel } from './CallbacksTabsTasking';
|
||||
import { CallbacksTabsFileBrowserLabel, CallbacksTabsFileBrowserPanel } from './CallbacksTabsFileBrowser';
|
||||
import { CallbacksTabsProcessBrowserLabel, CallbacksTabsProcessBrowserPanel } from './CallbacksTabsProcessBrowser';
|
||||
import { CallbacksTabsTaskingSplitLabel, CallbacksTabsTaskingSplitPanel} from "./CallbacksTabsTaskingSplit";
|
||||
import {CallbacksTabsTaskingConsoleLabel, CallbacksTabsTaskingConsolePanel} from "./CallbacksTabsTaskingConsole";
|
||||
import {
|
||||
CallbacksTabsCustomFileBasedBrowserLabel,
|
||||
CallbacksTabsCustomFileBasedBrowserPanel
|
||||
} from "./CallbacksTabsCustomFileBasedBrowser";
|
||||
|
||||
export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, onEditTabDescription, contextMenuOptions, me}) {
|
||||
export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clickedTabId, setClickedTabId, onEditTabDescription, contextMenuOptions, me, newDataForTab, setNewDataForTab}) {
|
||||
|
||||
const mountedRef = React.useRef(true);
|
||||
const [value, setValue] = React.useState(0);
|
||||
const handleChange = (event, newValue) => {
|
||||
setValue(newValue);
|
||||
//console.log(newValue);
|
||||
//console.log("clickedTab", newValue, openTabs[newValue]);
|
||||
if(openTabs[newValue] === undefined){return}
|
||||
localStorage.setItem('clickedTab', openTabs[newValue].tabID);
|
||||
setClickedTabId(openTabs[newValue].tabID);
|
||||
};
|
||||
React.useEffect( () => {
|
||||
return() => {
|
||||
@@ -24,107 +34,166 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, o
|
||||
}, [])
|
||||
const onCloseTabLocal = ({ tabID, index }) => {
|
||||
if (index > 0) {
|
||||
setClickedTabId(openTabs[index-1].tabID);
|
||||
setValue(index - 1);
|
||||
} else {
|
||||
//console.log("closing index", index)
|
||||
} else if(openTabs.length > 0) {
|
||||
//console.log("closing index length > 0", index)
|
||||
setClickedTabId(openTabs[0].tabID);
|
||||
setValue(0);
|
||||
}
|
||||
onCloseTab({ tabID, index });
|
||||
};
|
||||
useEffect(() => {
|
||||
//console.log(clickedTabId);
|
||||
//console.log(clickedTabId, openTabs);
|
||||
for (let i = 0; i < openTabs.length; i++) {
|
||||
//console.log("openTabs[i]", i, openTabs[i]);
|
||||
if (openTabs[i].tabID === clickedTabId) {
|
||||
//console.log("seting value", i);
|
||||
//console.log("setting value", i);
|
||||
setValue(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [clickedTabId, openTabs]);
|
||||
|
||||
const [collapseTaskRequest, setCollapseTaskRequest] = React.useState({})
|
||||
const modifiedInteractContextMenuOptions = [...contextMenuOptions,
|
||||
{
|
||||
name: 'Collapse All Tasks',
|
||||
click: ({event, index}) => {
|
||||
setCollapseTaskRequest((prevState) => {
|
||||
if(prevState[index] !== undefined){
|
||||
prevState[index] += 1;
|
||||
return {...prevState};
|
||||
} else {
|
||||
return { [index]: 0};
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
},]
|
||||
|
||||
return (
|
||||
<div style={{width: "100%", display: 'flex', flexDirection: 'column', flexGrow: 1, height: "100%" }}>
|
||||
<AppBar color='default' position='static' className={"no-box-shadow"}>
|
||||
<Tabs
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
indicatorColor='primary'
|
||||
textColor='primary'
|
||||
variant='scrollable'
|
||||
scrollButtons='auto'
|
||||
style={{ maxWidth: '100%', width: '100%' }}
|
||||
aria-label='scrollable auto tabs example'>
|
||||
{openTabs.map((tab, index) => {
|
||||
switch (tab.tabType) {
|
||||
case 'interact':
|
||||
return (
|
||||
<CallbacksTabsTaskingLabel
|
||||
onEditTabDescription={onEditTabDescription}
|
||||
onCloseTab={onCloseTabLocal}
|
||||
key={'tablabel' + tab.tabID + tab.tabType}
|
||||
tabInfo={tab}
|
||||
index={index}
|
||||
me={me}
|
||||
onDragTab={onDragTab}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
/>
|
||||
);
|
||||
case 'interactSplit':
|
||||
return (
|
||||
<CallbacksTabsTaskingSplitLabel
|
||||
onEditTabDescription={onEditTabDescription}
|
||||
onCloseTab={onCloseTabLocal}
|
||||
key={'tablabel' + tab.tabID + tab.tabType}
|
||||
tabInfo={tab}
|
||||
index={index}
|
||||
me={me}
|
||||
onDragTab={onDragTab}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
/>
|
||||
)
|
||||
case 'interactConsole':
|
||||
return (
|
||||
<CallbacksTabsTaskingConsoleLabel
|
||||
onEditTabDescription={onEditTabDescription}
|
||||
onCloseTab={onCloseTabLocal}
|
||||
key={'tablabel' + tab.tabID + tab.tabType}
|
||||
tabInfo={tab}
|
||||
index={index}
|
||||
me={me}
|
||||
onDragTab={onDragTab}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
/>
|
||||
)
|
||||
case 'fileBrowser':
|
||||
return (
|
||||
<CallbacksTabsFileBrowserLabel
|
||||
onEditTabDescription={onEditTabDescription}
|
||||
onCloseTab={onCloseTabLocal}
|
||||
key={'tablabel' + tab.tabID + tab.tabType}
|
||||
tabInfo={tab}
|
||||
index={index}
|
||||
me={me}
|
||||
onDragTab={onDragTab}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
/>
|
||||
);
|
||||
case 'processBrowser':
|
||||
return (
|
||||
<CallbacksTabsProcessBrowserLabel
|
||||
onEditTabDescription={onEditTabDescription}
|
||||
onCloseTab={onCloseTabLocal}
|
||||
key={'tablabel' + tab.tabID + tab.tabType}
|
||||
tabInfo={tab}
|
||||
index={index}
|
||||
me={me}
|
||||
onDragTab={onDragTab}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</Tabs>
|
||||
<div style={{width: "100%", maxWidth: "100%", display: 'flex', flexDirection: 'column', flexGrow: 1, height: "100%" }}>
|
||||
<AppBar color='default' position={"static"} style={{backgroundColor: "transparent"}} >
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="callback-tabs-list" direction={"horizontal"}>
|
||||
{(provided) => (
|
||||
<>
|
||||
<Tabs
|
||||
ref={provided.innerRef} {...provided.droppableProps}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
indicatorColor='primary'
|
||||
textColor='primary'
|
||||
variant='fullWidth'
|
||||
scrollButtons={true}
|
||||
style={{ }}
|
||||
TabIndicatorProps={{style: {display: "none"}}}
|
||||
aria-label='scrollable tabs'>
|
||||
{openTabs.map((tab, index) => {
|
||||
switch (tab.tabType) {
|
||||
case 'interact':
|
||||
return (
|
||||
<CallbacksTabsTaskingLabel
|
||||
onEditTabDescription={onEditTabDescription}
|
||||
onCloseTab={onCloseTabLocal}
|
||||
key={'tablabel' + tab.tabID + tab.tabType}
|
||||
tabInfo={tab}
|
||||
index={index}
|
||||
me={me}
|
||||
newDataForTab={newDataForTab}
|
||||
selectedIndex={value}
|
||||
onDragTab={onDragTab}
|
||||
contextMenuOptions={modifiedInteractContextMenuOptions}
|
||||
/>
|
||||
);
|
||||
case 'interactSplit':
|
||||
return (
|
||||
<CallbacksTabsTaskingSplitLabel
|
||||
onEditTabDescription={onEditTabDescription}
|
||||
onCloseTab={onCloseTabLocal}
|
||||
key={'tablabel' + tab.tabID + tab.tabType}
|
||||
tabInfo={tab}
|
||||
index={index}
|
||||
me={me}
|
||||
newDataForTab={newDataForTab}
|
||||
selectedIndex={value}
|
||||
onDragTab={onDragTab}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
/>
|
||||
)
|
||||
case 'interactConsole':
|
||||
return (
|
||||
<CallbacksTabsTaskingConsoleLabel
|
||||
onEditTabDescription={onEditTabDescription}
|
||||
onCloseTab={onCloseTabLocal}
|
||||
key={'tablabel' + tab.tabID + tab.tabType}
|
||||
tabInfo={tab}
|
||||
index={index}
|
||||
me={me}
|
||||
newDataForTab={newDataForTab}
|
||||
selectedIndex={value}
|
||||
onDragTab={onDragTab}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
/>
|
||||
)
|
||||
case 'customFileBasedBrowser':
|
||||
return (
|
||||
<CallbacksTabsCustomFileBasedBrowserLabel
|
||||
onEditTabDescription={onEditTabDescription}
|
||||
onCloseTab={onCloseTabLocal}
|
||||
key={'tablabel' + tab.tabID + tab.tabType + tab?.customBrowser?.name}
|
||||
tabInfo={tab}
|
||||
index={index}
|
||||
me={me}
|
||||
newDataForTab={newDataForTab}
|
||||
selectedIndex={value}
|
||||
onDragTab={onDragTab}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
/>
|
||||
);
|
||||
case 'processBrowser':
|
||||
return (
|
||||
<CallbacksTabsProcessBrowserLabel
|
||||
onEditTabDescription={onEditTabDescription}
|
||||
onCloseTab={onCloseTabLocal}
|
||||
key={'tablabel' + tab.tabID + tab.tabType}
|
||||
tabInfo={tab}
|
||||
index={index}
|
||||
me={me}
|
||||
newDataForTab={newDataForTab}
|
||||
selectedIndex={value}
|
||||
onDragTab={onDragTab}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
/>
|
||||
);
|
||||
case 'fileBrowser':
|
||||
return (
|
||||
<CallbacksTabsFileBrowserLabel
|
||||
onEditTabDescription={onEditTabDescription}
|
||||
onCloseTab={onCloseTabLocal}
|
||||
key={'tablabel' + tab.tabID + tab.tabType}
|
||||
tabInfo={tab}
|
||||
index={index}
|
||||
me={me}
|
||||
newDataForTab={newDataForTab}
|
||||
selectedIndex={value}
|
||||
onDragTab={onDragTab}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</Tabs>
|
||||
</>
|
||||
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</AppBar>
|
||||
|
||||
{openTabs.map((tab, index) => {
|
||||
@@ -141,9 +210,11 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, o
|
||||
key={'tabpanel' + tab.tabID + tab.tabType}
|
||||
onCloseTab={onCloseTabLocal}
|
||||
tabInfo={tab}
|
||||
setNewDataForTab={setNewDataForTab}
|
||||
value={value}
|
||||
index={index}
|
||||
me={me}
|
||||
collapseTaskRequest={collapseTaskRequest}
|
||||
parentMountedRef={mountedRef}
|
||||
/>
|
||||
);
|
||||
@@ -159,6 +230,7 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, o
|
||||
key={'tabpanel' + tab.tabID + tab.tabType}
|
||||
onCloseTab={onCloseTabLocal}
|
||||
tabInfo={tab}
|
||||
setNewDataForTab={setNewDataForTab}
|
||||
value={value}
|
||||
index={index}
|
||||
me={me}
|
||||
@@ -177,12 +249,32 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, o
|
||||
key={'tabpanel' + tab.tabID + tab.tabType}
|
||||
onCloseTab={onCloseTabLocal}
|
||||
tabInfo={tab}
|
||||
setNewDataForTab={setNewDataForTab}
|
||||
value={value}
|
||||
index={index}
|
||||
me={me}
|
||||
parentMountedRef={mountedRef}
|
||||
/>
|
||||
)
|
||||
case 'customFileBasedBrowser':
|
||||
return (
|
||||
<CallbacksTabsCustomFileBasedBrowserPanel
|
||||
style={{
|
||||
height: '100%',
|
||||
maxHeight: '100%',
|
||||
position: 'relative',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
onCloseTab={onCloseTabLocal}
|
||||
key={'tabpanel' + tab.tabID + tab.tabType + tab?.customBrowser?.name}
|
||||
tabInfo={tab}
|
||||
setNewDataForTab={setNewDataForTab}
|
||||
value={value}
|
||||
index={index}
|
||||
me={me}
|
||||
parentMountedRef={mountedRef}
|
||||
/>
|
||||
);
|
||||
case 'fileBrowser':
|
||||
return (
|
||||
<CallbacksTabsFileBrowserPanel
|
||||
@@ -195,10 +287,12 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, o
|
||||
onCloseTab={onCloseTabLocal}
|
||||
key={'tabpanel' + tab.tabID + tab.tabType}
|
||||
tabInfo={tab}
|
||||
setNewDataForTab={setNewDataForTab}
|
||||
value={value}
|
||||
index={index}
|
||||
me={me}
|
||||
parentMountedRef={mountedRef}
|
||||
baseUIFeature={"file_browser"}
|
||||
/>
|
||||
);
|
||||
case 'processBrowser':
|
||||
@@ -213,6 +307,7 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, clickedTabId, o
|
||||
onCloseTab={onCloseTabLocal}
|
||||
key={'tabpanel' + tab.tabID + tab.tabType}
|
||||
tabInfo={tab}
|
||||
setNewDataForTab={setNewDataForTab}
|
||||
value={value}
|
||||
index={index}
|
||||
me={me}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+976
@@ -0,0 +1,976 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useMutation, useLazyQuery, gql } from '@apollo/client';
|
||||
import { snackActions } from '../../utilities/Snackbar';
|
||||
import {
|
||||
MythicDialog,
|
||||
MythicModifyStringDialog,
|
||||
MythicViewJSONAsTableDialog,
|
||||
} from '../../MythicComponents/MythicDialog';
|
||||
import {faFolder} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import DescriptionIcon from '@mui/icons-material/Description';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import ErrorIcon from '@mui/icons-material/Error';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { IconButton } from '@mui/material';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import ListIcon from '@mui/icons-material/List';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import GetAppIcon from '@mui/icons-material/GetApp';
|
||||
import 'react-virtualized/styles.css';
|
||||
import FileCopyOutlinedIcon from '@mui/icons-material/FileCopyOutlined';
|
||||
import { copyStringToClipboard } from '../../utilities/Clipboard';
|
||||
import MythicResizableGrid from '../../MythicComponents/MythicResizableGrid';
|
||||
import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip';
|
||||
import {faFilter} from '@fortawesome/free-solid-svg-icons';
|
||||
import {MythicTransferListDialog} from '../../MythicComponents/MythicTransferList';
|
||||
import {TagsDisplay, ViewEditTags} from '../../MythicComponents/MythicTag';
|
||||
import { toLocalTime } from '../../utilities/Time';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import {RenderSingleTask} from "../SingleTaskView/SingleTaskView";
|
||||
import {
|
||||
GetComputedFontSize,
|
||||
GetMythicSetting,
|
||||
useSetMythicSetting
|
||||
} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {getIconColor, getIconName} from "./ResponseDisplayTable";
|
||||
import {CallbacksTableColumnsReorderDialog} from "./CallbacksTableColumnsReorderDialog";
|
||||
|
||||
const updateFileComment = gql`
|
||||
mutation updateCommentMutation($mythictree_id: Int!, $comment: String!) {
|
||||
update_mythictree_by_pk(pk_columns: { id: $mythictree_id }, _set: { comment: $comment }) {
|
||||
comment
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CallbacksTabsCustomFileBasedBrowserTable = (props) => {
|
||||
const theme = useTheme();
|
||||
const [updateSetting] = useSetMythicSetting();
|
||||
const [allData, setAllData] = React.useState([]);
|
||||
const [openReorderDialog, setOpenReorderDialog] = React.useState(false);
|
||||
const [openContextMenu, setOpenContextMenu] = React.useState(false);
|
||||
const [filterOptions, setFilterOptions] = React.useState({});
|
||||
const [selectedColumn, setSelectedColumn] = React.useState({});
|
||||
const [fileCommentDialogOpen, setFileCommentDialogOpen] = React.useState(false);
|
||||
const [viewPermissionsDialogOpen, setViewPermissionsDialogOpen] = React.useState(false);
|
||||
const permissionDataRef = React.useRef({metadata: {}, name: ""});
|
||||
const commentDataRef = React.useRef({id: 0, comment: ""});
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [sortData, setSortData] = React.useState({"sortKey": null, "sortDirection": null, "sortType": null})
|
||||
const [columnVisibility, setColumnVisibility] = React.useState({
|
||||
"visible": [...props.treeConfig.table.visible],
|
||||
"hidden": [...props.treeConfig.table.hidden]
|
||||
});
|
||||
const indicatePartialListingRef = React.useRef(props.treeConfig.indicate_partial_listing);
|
||||
const [selectedRows, setSelectedRows] = React.useState([]);
|
||||
const [updateComment] = useMutation(updateFileComment, {
|
||||
onCompleted: (data) => {
|
||||
snackActions.success('updated comment');
|
||||
},
|
||||
});
|
||||
const onSubmitUpdatedComment = (comment) => {
|
||||
updateComment({ variables: { mythictree_id: commentDataRef.current.id, comment: comment } });
|
||||
};
|
||||
const [initialDefaultColumns, setInitialDefaultColumns] = React.useState([
|
||||
{ name: 'name', type: 'name', key: 'name_text', fillWidth: true, visible: true },
|
||||
...props.treeConfig.table.columns,
|
||||
{ name: 'tags', type: 'tags', key: 'tags', disableSort: true, disableFilterMenu: true, width: 220 },
|
||||
{ name: 'comment', type: 'comment', key: 'comment', width: 200 },
|
||||
]);
|
||||
const [columnOrder, setColumnOrder] = React.useState(initialDefaultColumns);
|
||||
const columns = React.useMemo(
|
||||
() =>
|
||||
columnOrder.reduce( (prev, cur) => {
|
||||
if(columnVisibility.visible.includes(cur.name)){
|
||||
if(filterOptions[cur.key] && String(filterOptions[cur.key]).length > 0){
|
||||
return [...prev, {...cur, filtered: true}];
|
||||
}else{
|
||||
return [...prev, {...cur}];
|
||||
}
|
||||
}else{
|
||||
return [...prev];
|
||||
}
|
||||
}, [])
|
||||
, [filterOptions, columnVisibility, columnOrder]
|
||||
);
|
||||
|
||||
const sortedData = React.useMemo(() => {
|
||||
if (sortData.sortKey === null || sortData.sortType === null) {
|
||||
return allData;
|
||||
}
|
||||
const tempData = [...allData];
|
||||
if (sortData.sortType === 'number' || sortData.sortType === 'size' || sortData.sortType === 'date') {
|
||||
tempData.sort((a, b) => {
|
||||
if(sortData.inMetadata){
|
||||
try {
|
||||
if(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a]?.metadata[sortData.sortKey] ===
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b]?.metadata[sortData.sortKey]){
|
||||
return 0;
|
||||
}
|
||||
if (props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a]?.metadata[sortData.sortKey] === null ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a]?.metadata[sortData.sortKey] === undefined) {
|
||||
return -1;
|
||||
}
|
||||
if (props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b]?.metadata[sortData.sortKey] === null ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b]?.metadata[sortData.sortKey] === undefined) {
|
||||
return 1;
|
||||
}
|
||||
return parseInt(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a]?.metadata[sortData.sortKey]) >
|
||||
parseInt(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b]?.metadata[sortData.sortKey]) ? 1 : -1;
|
||||
}catch(error) {
|
||||
console.log("failed to parse data for sorting", error);
|
||||
return props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a]?.metadata[sortData.sortKey] >
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b]?.metadata[sortData.sortKey] ? 1 : -1
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
if(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] ===
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey]){
|
||||
return 0;
|
||||
}
|
||||
if (props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] === null ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] === undefined) {
|
||||
return -1;
|
||||
}
|
||||
if (props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey] === null ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey] === undefined) {
|
||||
return 1;
|
||||
}
|
||||
return parseInt(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey]) > parseInt(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey]) ? 1 : -1;
|
||||
} catch (error) {
|
||||
console.log("failed to parse data for sorting", error);
|
||||
return props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] > props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
} else if (sortData.sortType === 'string') {
|
||||
tempData.sort((a, b) => {
|
||||
try{
|
||||
if(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] ===
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey]){
|
||||
return 0;
|
||||
}
|
||||
if (props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] === null ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] === undefined) {
|
||||
return -1;
|
||||
}
|
||||
if (props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey] === null ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey] === undefined) {
|
||||
return 1;
|
||||
}
|
||||
return props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey].localeCompare(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey]);
|
||||
}catch(error){
|
||||
console.log("failed to parse data for sorting", error);
|
||||
return props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][a][sortData.sortKey] > props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][b][sortData.sortKey] ? 1 : -1
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
if (sortData.sortDirection === 'DESC') {
|
||||
tempData.reverse();
|
||||
}
|
||||
return tempData;
|
||||
}, [allData, sortData]);
|
||||
const onSubmitFilterOptions = (value) => {
|
||||
setFilterOptions({...filterOptions, [selectedColumn.key]: value });
|
||||
try{
|
||||
updateSetting({setting_name: `${props.treeConfig.name}_browser_filter_options`, value: {...filterOptions, [selectedColumn.key]: value }});
|
||||
}catch(error){
|
||||
console.log("failed to save filter options");
|
||||
}
|
||||
}
|
||||
const filterRow = (row) => {
|
||||
if(props.treeRootData[props.selectedFolderData.group] === undefined ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host] === undefined ||
|
||||
props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row] === undefined) {
|
||||
return true;
|
||||
}
|
||||
if(!props.showDeletedFiles && props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.deleted){
|
||||
return true;
|
||||
}
|
||||
for(const [key,value] of Object.entries(filterOptions)){
|
||||
if(["name_text", "comment"].includes(key)){
|
||||
if(!String(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row][key]).toLowerCase().includes(value)){
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if(!String(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row].metadata[key]).toLowerCase().includes(value)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const gridData = React.useMemo(
|
||||
() =>
|
||||
// row is just the name
|
||||
sortedData.reduce((prev, row) => {
|
||||
if(filterRow(row)){
|
||||
return [...prev];
|
||||
}else{
|
||||
return [...prev, columns.map( c => {
|
||||
switch(c.type) {
|
||||
case "name":
|
||||
return <FileBrowserTableRowNameCell
|
||||
treeRootData={props.treeRootData[props.selectedFolderData.group]}
|
||||
selectedFolderData={props.selectedFolderData}
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row].name_text}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}/>;
|
||||
case "tags":
|
||||
return <FileBrowserTagsCell
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}
|
||||
treeRootData={props.treeRootData[props.selectedFolderData.group]}
|
||||
cellData={row}
|
||||
selectedFolderData={props.selectedFolderData}
|
||||
me={props.me}/>
|
||||
case "comment":
|
||||
return <FileBrowserTableRowStringCell
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row].comment}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}/>
|
||||
case "date":
|
||||
return <TableRowDateCell
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.metadata?.[c.key] || null}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}
|
||||
view_utc_time={props.me?.user?.view_utc_time}/>
|
||||
case "size":
|
||||
return <TableRowSizeCell
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.metadata?.[c.key] || 0}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}/>
|
||||
case "string":
|
||||
return <FileBrowserTableRowStringCell
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.metadata?.[c.key] || ""}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}/>
|
||||
default:
|
||||
return <FileBrowserTableRowStringCell
|
||||
cellData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]?.metadata?.[c.key] || ""}
|
||||
rowData={props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row]}/>
|
||||
}
|
||||
})];
|
||||
}
|
||||
|
||||
}, []),
|
||||
[sortedData, props.onTaskRowAction, filterOptions, columnVisibility, props.showDeletedFiles]
|
||||
);
|
||||
const getDisplayFormat = () => {
|
||||
if(sortedData.length === 0 && props?.selectedFolderData?.success === false){
|
||||
return "showTask";
|
||||
}else if(sortedData.length > 0 || props?.selectedFolderData?.success){
|
||||
return "normal";
|
||||
} else if(sortedData.length === 0 && props?.selectedFolderData?.has_children && props?.selectedFolderData?.success === null){
|
||||
return "fetchLocal";
|
||||
}else if(sortedData.length === 0 && !props?.selectedFolderData?.has_children && props?.selectedFolderData?.success === null) {
|
||||
return "fetchRemote";
|
||||
}else if(sortedData.length === 0){
|
||||
return "fetchRemote";
|
||||
} else {
|
||||
return "normal";
|
||||
}
|
||||
}
|
||||
const displayFormat = getDisplayFormat();
|
||||
useEffect(() => {
|
||||
// when the folder changes, we need to aggregate all of the entries
|
||||
//console.log(props.selectedFolderData, props.treeAdjMatrix, props.treeRootData)
|
||||
let desiredPath = props.selectedFolderData.full_path_text;
|
||||
if(props.selectedFolderData.id === props.selectedFolderData.host){
|
||||
desiredPath = "";
|
||||
}
|
||||
let newAllData = Object.keys(props.treeAdjMatrix[props.selectedFolderData.group]?.[props.selectedFolderData.host]?.[desiredPath] || {});
|
||||
setAllData(newAllData);
|
||||
//console.log("just set all data")
|
||||
}, [props.selectedFolderData, props.treeAdjMatrix]);
|
||||
const onRowDoubleClick = (e, rowIndex, rowData) => {
|
||||
if (!rowData.can_have_children) {
|
||||
return;
|
||||
}
|
||||
snackActions.info('Fetching contents from database...');
|
||||
props.onRowDoubleClick({...rowData, group: props.selectedFolderData.group});
|
||||
setSortData({"sortKey": null, "sortType":null, "sortDirection": "ASC"});
|
||||
};
|
||||
const onClickHeader = (e, columnIndex) => {
|
||||
const column = columns[columnIndex];
|
||||
if(column.disableSort){
|
||||
return;
|
||||
}
|
||||
if (!column.key) {
|
||||
setSortData({"sortKey": null, "sortType":null, "sortDirection": "ASC", "inMetadata": false});
|
||||
}
|
||||
if (sortData.sortKey === column.key) {
|
||||
if (sortData.sortDirection === 'ASC') {
|
||||
setSortData({...sortData, "sortDirection": "DESC"});
|
||||
} else {
|
||||
setSortData({"sortKey": null, "sortType":null, "sortDirection": "ASC", "inMetadata": false});
|
||||
}
|
||||
} else {
|
||||
setSortData({"sortKey": column.key, "inMetadata": column.inMetadata, "sortType":column.type, "sortDirection": "ASC"});
|
||||
}
|
||||
};
|
||||
const contextMenuOptions = [
|
||||
{
|
||||
name: 'Filter Column', type: "item",
|
||||
icon: <FontAwesomeIcon icon={faFilter} style={{paddingRight: "5px"}} />,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
if(columns[columnIndex].disableFilterMenu){
|
||||
snackActions.warning("Can't filter that column");
|
||||
return;
|
||||
}
|
||||
setSelectedColumn(columns[columnIndex]);
|
||||
setOpenContextMenu(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Reorder Columns and Adjust Visibility", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
setOpenReorderDialog(true);
|
||||
}
|
||||
}
|
||||
];
|
||||
const sortColumn = columns.findIndex((column) => column.key === sortData.sortKey);
|
||||
const onRowClick = ({event, rowDataStatic}) => {
|
||||
if(event.ctrlKey || event.metaKey){
|
||||
if(props.treeRootData?.[props.selectedFolderData.group]?.[rowDataStatic.host]?.[rowDataStatic.full_path_text]){
|
||||
props.treeRootData[props.selectedFolderData.group][rowDataStatic.host][rowDataStatic.full_path_text].selected = true;
|
||||
}
|
||||
|
||||
setSelectedRows([...selectedRows, rowDataStatic]);
|
||||
} else {
|
||||
if(props.treeRootData?.[props.selectedFolderData.group]?.[rowDataStatic.host]?.[rowDataStatic.full_path_text]){
|
||||
props.treeRootData[props.selectedFolderData.group][rowDataStatic.host][rowDataStatic.full_path_text].selected = true;
|
||||
}
|
||||
for(let i = 0; i < selectedRows.length; i++){
|
||||
if(props.treeRootData?.[props.selectedFolderData.group]?.[selectedRows[i].host]?.[selectedRows[i].full_path_text]){
|
||||
props.treeRootData[props.selectedFolderData.group][selectedRows[i].host][selectedRows[i].full_path_text].selected = false;
|
||||
}
|
||||
}
|
||||
setSelectedRows([rowDataStatic]);
|
||||
}
|
||||
}
|
||||
const optionsA = (element) => [
|
||||
{
|
||||
name: element?.name_text, icon: null, click: ({event}) => {},
|
||||
type: "item", disabled: true
|
||||
},
|
||||
{
|
||||
name: "Copy to Clipboard", icon: null, click: () => {}, type: "menu",
|
||||
menuItems: [
|
||||
{
|
||||
name: 'Name', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(element.name_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Full Path', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(element.full_path_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Metadata', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(JSON.stringify(element?.metadata, null, 2))){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'View Metadata', type: "item",
|
||||
icon: <VisibilityIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
permissionDataRef.current.metadata = element?.metadata;
|
||||
permissionDataRef.current.name = element.name_text;
|
||||
setViewPermissionsDialogOpen(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Edit Comment', type: "item",
|
||||
icon: <EditIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
commentDataRef.current.id = element.id;
|
||||
commentDataRef.current.comment = element.comment;
|
||||
setFileCommentDialogOpen(true);
|
||||
},
|
||||
},
|
||||
]
|
||||
async function optionsB (callback_id, callback_display_id, element) {
|
||||
let downloadCommand = await props.getLoadedCommandForUIFeature(callback_id, element.can_have_children ? props.baseUIFeature + ":download_folder" : props.baseUIFeature + ":download");
|
||||
let downloadDisplay = "Download (Unsupported)";
|
||||
if(element.can_have_children){
|
||||
downloadDisplay = "Download Folder (Unsupported)";
|
||||
}
|
||||
if(downloadCommand !== undefined){
|
||||
downloadDisplay = `Download (${downloadCommand.command.cmd})`;
|
||||
if(element.can_have_children){
|
||||
downloadDisplay = `Download Folder (${downloadCommand.command.cmd})`;
|
||||
}
|
||||
}
|
||||
let listCommand = await props.getLoadedCommandForUIFeature(callback_id, props.baseUIFeature + ":list");
|
||||
let listDisplay = "List (Unsupported)";
|
||||
if(listCommand !== undefined){
|
||||
listDisplay = `List (${listCommand.command.cmd})`;
|
||||
}
|
||||
let removeCommand = await props.getLoadedCommandForUIFeature(callback_id, props.baseUIFeature + ":remove");
|
||||
let removeDisplay = "Remove (Unsupported)";
|
||||
if(removeCommand !== undefined){
|
||||
removeDisplay = `Remove (${removeCommand.command.cmd})`;
|
||||
}
|
||||
return [
|
||||
{
|
||||
name: downloadDisplay, type: "item",
|
||||
disabled: downloadCommand === undefined,
|
||||
icon: <GetAppIcon color="success" style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
props.onTaskRowAction({
|
||||
path: element.parent_path_text,
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: element.can_have_children ? props.baseUIFeature + ':download_folder' : props.baseUIFeature + ':download',
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: listDisplay, type: "item",
|
||||
disabled: listCommand === undefined,
|
||||
icon: <ListIcon color="warning" style={{ paddingRight: '5px'}} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
props.onTaskRowAction({
|
||||
path: element.parent_path_text,
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: props.baseUIFeature + ':list',
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: removeDisplay, type: "item",
|
||||
disabled: removeCommand === undefined,
|
||||
icon: <DeleteIcon color="error" style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
props.onTaskRowAction({
|
||||
path: element.parent_path_text,
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: props.baseUIFeature + ':remove',
|
||||
getConfirmation: true,
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
async function optionsC (callback_id, callback_display_id, element, action_name, ui_feature, icon, color, openDialog, getConfirmation) {
|
||||
let command = await props.getLoadedCommandForUIFeature(callback_id, ui_feature);
|
||||
let commandDisplay = `${action_name} (Unsupported)`;
|
||||
if(command !== undefined){
|
||||
commandDisplay = `${action_name} (${command.command.cmd})`;
|
||||
}
|
||||
return {
|
||||
name: commandDisplay, type: "item",
|
||||
disabled: command === undefined,
|
||||
icon: <FontAwesomeIcon icon={getIconName(icon)} style={{marginRight: "5px", color: getIconColor(theme, color || "")}}/>,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
props.onTaskRowAction({
|
||||
path: element.parent_path_text,
|
||||
full_path: element.full_path_text,
|
||||
host: element.host,
|
||||
filename: element.name_text,
|
||||
uifeature: ui_feature, openDialog, getConfirmation,
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
async function onRowContextClick({rowDataStatic}) {
|
||||
// based on row, return updated options array?
|
||||
if(selectedRows.length > 1){
|
||||
let downloadCommand = await props.getLoadedCommandForUIFeature(rowDataStatic.callback.id, rowDataStatic.can_have_children ? props.baseUIFeature + ":download_folder" : props.baseUIFeature + ":download");
|
||||
let listCommand = await props.getLoadedCommandForUIFeature(rowDataStatic.callback.id, props.baseUIFeature + ":list");
|
||||
let removeCommand = await props.getLoadedCommandForUIFeature(rowDataStatic.callback.id, props.baseUIFeature + ":remove");
|
||||
return [
|
||||
{
|
||||
name: `Download All Selected`, type: "item",
|
||||
disabled: downloadCommand === undefined,
|
||||
icon: <GetAppIcon color="success" style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
let newTasks = [];
|
||||
for(let i = 0; i < selectedRows.length; i++){
|
||||
newTasks.push({
|
||||
path: selectedRows[i].parent_path_text,
|
||||
full_path: selectedRows[i].full_path_text,
|
||||
host: selectedRows[i].host,
|
||||
filename: selectedRows[i].name_text,
|
||||
uifeature: selectedRows[i].can_have_children ? props.baseUIFeature + ':download_folder': props.baseUIFeature + ':download',
|
||||
callback_id: props.tabInfo.callbackID,
|
||||
callback_display_id: props.tabInfo.displayID
|
||||
});
|
||||
}
|
||||
props.onTaskRowActions(newTasks);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `Remove All Selected`, type: "item",
|
||||
disabled: removeCommand === undefined,
|
||||
icon: <DeleteIcon color="error" style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
let newTasks = [];
|
||||
for(let i = 0; i < selectedRows.length; i++){
|
||||
newTasks.push({
|
||||
path: selectedRows[i].parent_path_text,
|
||||
full_path: selectedRows[i].full_path_text,
|
||||
host: selectedRows[i].host,
|
||||
filename: selectedRows[i].name_text,
|
||||
uifeature: props.baseUIFeature + ':remove',
|
||||
getConfirmation: true,
|
||||
callback_id: props.tabInfo.callbackID,
|
||||
callback_display_id: props.tabInfo.displayID
|
||||
});
|
||||
}
|
||||
props.onTaskRowActions(newTasks);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `List All Selected`, type: "item",
|
||||
disabled: listCommand === undefined,
|
||||
icon: <ListIcon color="warning" style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
let newTasks = [];
|
||||
for(let i = 0; i < selectedRows.length; i++){
|
||||
newTasks.push({
|
||||
path: selectedRows[i].parent_path_text,
|
||||
full_path: selectedRows[i].full_path_text,
|
||||
host: selectedRows[i].host,
|
||||
filename: selectedRows[i].name_text,
|
||||
uifeature: props.baseUIFeature + ':list',
|
||||
callback_id: props.tabInfo.callbackID,
|
||||
callback_display_id: props.tabInfo.displayID
|
||||
});
|
||||
}
|
||||
props.onTaskRowActions(newTasks);
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
let options = [...optionsA(rowDataStatic)];
|
||||
options.push(...(await optionsB(props.tabInfo.callbackID, props.tabInfo.displayID, rowDataStatic)));
|
||||
if(props.treeConfig.row_actions.length > 0){
|
||||
let extraOptions = [];
|
||||
for(let i = 0; i < props.treeConfig.row_actions.length; i++){
|
||||
let r = props.treeConfig.row_actions[i];
|
||||
if((r.supports_file && !rowDataStatic.can_have_children) || (r.supports_folder && rowDataStatic.can_have_children) ){
|
||||
extraOptions.push(await optionsC(rowDataStatic.callback.id, rowDataStatic.callback.display_id,
|
||||
rowDataStatic, r.name, r.ui_feature, r.icon, r.color, r.openDialog, r.getConfirmation));
|
||||
}
|
||||
}
|
||||
if(extraOptions.length > 0){
|
||||
options.push({
|
||||
name: `Custom Actions`, icon: null, click: () => {}, type: "menu",
|
||||
menuItems: extraOptions
|
||||
})
|
||||
}
|
||||
}
|
||||
if(rowDataStatic.callback.display_id !== props.tabInfo.displayID){
|
||||
options.push({
|
||||
name: `Original Callback: ${rowDataStatic.callback.display_id}`, icon: null, click: () => {}, type: "menu",
|
||||
menuItems: [
|
||||
...(await optionsB(rowDataStatic.callback.id, rowDataStatic.callback.display_id, rowDataStatic))
|
||||
]
|
||||
})
|
||||
}
|
||||
return options;
|
||||
}
|
||||
React.useEffect( () => {
|
||||
// on startup, want to see if `callbacks_table_columns` exists in storage and load it if possible
|
||||
try {
|
||||
const storageItem = GetMythicSetting({setting_name: `${props.treeConfig.name}_browser_table_columns`, default_value: [...props.treeConfig.table.visible]});
|
||||
if(storageItem !== null){
|
||||
let allColumns = [...columnVisibility["visible"].map(c => c), ...columnVisibility["hidden"].map(c => c)];
|
||||
let newHidden = [];
|
||||
allColumns.forEach((v,i,a) => {
|
||||
if(!storageItem.includes(v)){
|
||||
newHidden.push(v);
|
||||
}
|
||||
});
|
||||
if(storageItem.length !== 0){
|
||||
setColumnVisibility({visible: storageItem, hidden: newHidden});
|
||||
}
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_columns", error);
|
||||
}
|
||||
try {
|
||||
const storageItemOptions = GetMythicSetting({setting_name: `${props.treeConfig.name}_browser_filter_options`, default_value: {}});
|
||||
if(storageItemOptions !== null){
|
||||
setFilterOptions(storageItemOptions);
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_filter_options", error);
|
||||
}
|
||||
try {
|
||||
const storageColumnOrder = GetMythicSetting({setting_name: `${props.treeConfig.name}_browser_column_order`, default_value: columns.map(c => c.name)});
|
||||
if(storageColumnOrder !== null){
|
||||
let newOrder = [];
|
||||
for(let i = 0; i < storageColumnOrder.length; i++){
|
||||
for(let j = 0; j < columnOrder.length; j++){
|
||||
if(columnOrder[j].name === storageColumnOrder[i]){
|
||||
newOrder.push(columnOrder[j]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(newOrder.length === columns.length){
|
||||
setColumnOrder(newOrder);
|
||||
}
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_filter_options", error);
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
const onSubmitColumnReorder = (newOrder) => {
|
||||
let newVisible = [];
|
||||
let newHidden = [];
|
||||
for(let i = 0; i < newOrder.length; i++){
|
||||
if(newOrder[i].visible){
|
||||
newVisible.push(newOrder[i].name);
|
||||
} else {
|
||||
newHidden.push(newOrder[i].name);
|
||||
}
|
||||
}
|
||||
if(newVisible.length === 0){
|
||||
snackActions.error("Can't update to show no fields");
|
||||
return;
|
||||
}
|
||||
updateSetting({setting_name: `${props.treeConfig.name}_browser_column_order`, value: newOrder.map(c => c.name)});
|
||||
setColumnOrder(newOrder);
|
||||
setColumnVisibility({visible: newVisible, hidden: newHidden});
|
||||
updateSetting({setting_name: `${props.treeConfig.name}_browser_table_columns`, value: newVisible});
|
||||
setOpenReorderDialog(false);
|
||||
}
|
||||
const onResetColumnReorder = () => {
|
||||
onSubmitColumnReorder(initialDefaultColumns);
|
||||
}
|
||||
if(loading){
|
||||
return (
|
||||
<div style={{width: '100%', height: '100%', position: "relative",}}>
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%"
|
||||
}}>
|
||||
{"Loading Saved Browser Customizations..."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div style={{width: '100%', height: '100%', overflow: "hidden", position: "relative"}}>
|
||||
{displayFormat === "normal" &&
|
||||
<>
|
||||
<MythicResizableGrid
|
||||
columns={columns}
|
||||
sortIndicatorIndex={sortColumn}
|
||||
sortDirection={sortData.sortDirection}
|
||||
items={gridData}
|
||||
rowHeight={GetComputedFontSize() + 7}
|
||||
onClickHeader={onClickHeader}
|
||||
onDoubleClickRow={onRowDoubleClick}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
onRowContextMenuClick={onRowContextClick}
|
||||
onRowClick={onRowClick}
|
||||
/>
|
||||
{fileCommentDialogOpen && (
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
maxWidth='md'
|
||||
open={fileCommentDialogOpen}
|
||||
onClose={() => {
|
||||
setFileCommentDialogOpen(false);
|
||||
}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Edit Comment'
|
||||
onSubmit={onSubmitUpdatedComment}
|
||||
value={commentDataRef.current?.value}
|
||||
onClose={() => {
|
||||
setFileCommentDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{viewPermissionsDialogOpen && (
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
maxWidth='lg'
|
||||
open={viewPermissionsDialogOpen}
|
||||
onClose={() => {
|
||||
setViewPermissionsDialogOpen(false);
|
||||
}}
|
||||
innerDialog={
|
||||
<MythicViewJSONAsTableDialog
|
||||
title={'View Metadata for ' + permissionDataRef.current?.name}
|
||||
leftColumn='Attribute'
|
||||
rightColumn='Value'
|
||||
value={permissionDataRef.current?.metadata}
|
||||
onClose={() => {
|
||||
setViewPermissionsDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{openReorderDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openReorderDialog}
|
||||
onClose={()=>{setOpenReorderDialog(false);}}
|
||||
innerDialog={
|
||||
<CallbacksTableColumnsReorderDialog
|
||||
onClose={()=>{setOpenReorderDialog(false);}}
|
||||
visible={columnVisibility.visible}
|
||||
hidden={columnVisibility.hidden}
|
||||
onReset={onResetColumnReorder}
|
||||
onSubmit={onSubmitColumnReorder}
|
||||
initialItems={columnOrder}
|
||||
/>}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
}
|
||||
{displayFormat === "normal" && props?.selectedFolderData?.success === null && indicatePartialListingRef.current &&
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid black",
|
||||
padding: "5px",
|
||||
backgroundColor: "rgba(37,37,37,0.92)", color: "white",
|
||||
}}>
|
||||
{"Only PARTIAL data has been collected for this path. "}<br/>
|
||||
{"Task this callback to list the contents"}
|
||||
<IconButton style={{margin: 0, padding: 0, marginRight: "10px"}}
|
||||
onClick={props.onListFilesButtonFromTableWithNoEntries}>
|
||||
<RefreshIcon color={"info"} fontSize={"large"}
|
||||
style={{display: "inline-block",}}/>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{displayFormat === "fetchLocal" && indicatePartialListingRef.current &&
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid black",
|
||||
padding: "5px",
|
||||
backgroundColor: "rgba(37,37,37,0.92)", color: "white",
|
||||
}}>
|
||||
{"Some data exists for this path, but isn't loaded into the UI. "}
|
||||
<br/>
|
||||
{"Click the folder icon to fetch data from the database."}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{displayFormat === "fetchRemote" && indicatePartialListingRef.current &&
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid black",
|
||||
padding: "5px",
|
||||
backgroundColor: "rgba(37,37,37,0.92)", color: "white",
|
||||
}}>
|
||||
{"No data has been collected for this path. "}
|
||||
<div style={{display: "flex", alignItems: "center"}}>
|
||||
{"Task this callback to list the contents"}
|
||||
<IconButton style={{margin: 0, padding: 0, marginRight: "10px"}} onClick={props.onListFilesButtonFromTableWithNoEntries} >
|
||||
<RefreshIcon color={"info"} fontSize={"large"}
|
||||
style={{ display: "inline-block",}} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{displayFormat === "showTask" && props.selectedFolderData?.task_id > 0 &&
|
||||
<RenderSingleTask task_id={props.selectedFolderData?.task_id} />
|
||||
}
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openContextMenu}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={filterOptions[selectedColumn.key]}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const FileBrowserTableRowNameCell = ({cellData, rowData, treeRootData, selectedFolderData }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div style={{ alignItems: 'center', display: 'flex', maxHeight: "100%", textDecoration: treeRootData[selectedFolderData.host][rowData.full_path_text]?.deleted ? 'line-through' : '' }}>
|
||||
{!treeRootData[selectedFolderData.host][rowData.full_path_text]?.can_have_children ? (
|
||||
<DescriptionIcon size={"small"} style={{
|
||||
marginRight: '5px' }} />
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={faFolder}
|
||||
size={"1x"}
|
||||
style={{
|
||||
marginRight: '8px',
|
||||
marginLeft: "4px",
|
||||
color:
|
||||
treeRootData[selectedFolderData.host][rowData.full_path_text]?.success || treeRootData[selectedFolderData.host][rowData.full_path_text]?.has_children
|
||||
? theme.folderColor
|
||||
: theme.emptyFolderColor,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<pre
|
||||
style={{
|
||||
color:
|
||||
treeRootData[selectedFolderData.host][rowData.full_path_text]?.success !== null
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary,
|
||||
}}>
|
||||
{cellData}
|
||||
</pre>
|
||||
{treeRootData[selectedFolderData.host][rowData.full_path_text]?.success === true ? (
|
||||
<MythicStyledTooltip title='Successfully listed contents of folder' tooltipStyle={{display: "inline-flex", marginLeft: "5px"}}>
|
||||
<CheckCircleOutlineIcon color="success" fontSize='small' />
|
||||
</MythicStyledTooltip>
|
||||
) : treeRootData[selectedFolderData.host][rowData.full_path_text]?.success === false ? (
|
||||
<MythicStyledTooltip title='Failed to list contents of folder' tooltipStyle={{display: "inline-flex", marginLeft: "5px"}}>
|
||||
<ErrorIcon fontSize='small' color="error" />
|
||||
</MythicStyledTooltip>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const FileBrowserTagsCell = ({rowData, cellData, treeRootData, selectedFolderData, me}) => {
|
||||
return (
|
||||
<>
|
||||
<ViewEditTags target_object={"mythictree_id"} target_object_id={treeRootData[selectedFolderData.host][cellData]?.id} me={me} />
|
||||
<TagsDisplay tags={treeRootData[selectedFolderData.host][cellData]?.tags} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
const FileBrowserTableRowStringCell = ({ cellData, rowData }) => {
|
||||
//console.log(rowData, cellData)
|
||||
return (
|
||||
<>
|
||||
{cellData}
|
||||
</>
|
||||
)
|
||||
};
|
||||
export const TableRowDateCell = ({ cellData, rowData, view_utc_time }) => {
|
||||
|
||||
try{
|
||||
let cellDataInt = parseInt(cellData)
|
||||
if(cellData === "" || cellData === undefined || cellDataInt <= 0){
|
||||
return "";
|
||||
}
|
||||
let view_utc = true;
|
||||
if(view_utc_time !== undefined){
|
||||
view_utc = view_utc_time
|
||||
}
|
||||
// handle Unix epoch timestamps
|
||||
if (view_utc) {
|
||||
let init_date = new Date(cellDataInt);
|
||||
return init_date.toDateString() + " " + init_date.toTimeString().substring(0, 8) + " UTC";
|
||||
} else {
|
||||
let timezoneDate = new Date(cellDataInt);
|
||||
timezoneDate.setTime(timezoneDate.getTime() - (timezoneDate.getTimezoneOffset() *60*1000));
|
||||
return timezoneDate.toLocaleDateString() + " " + timezoneDate.toLocaleString([], {hour12: true, hour: "2-digit", minute: "2-digit"});
|
||||
}
|
||||
//const dateData = new Date(cellDataInt).toISOString();
|
||||
//return toLocalTime(dateData.slice(0, 10) + " " + dateData.slice(11,-1), view_utc_time);
|
||||
}catch(error){
|
||||
try{
|
||||
let cellDataInt = parseInt(cellData)
|
||||
// handle windows FILETIME values
|
||||
const dateData = new Date( ((cellDataInt / 10000000) - 11644473600) * 1000).toISOString();
|
||||
return toLocalTime(dateData.slice(0, 10) + " " + dateData.slice(11,-1), view_utc_time);
|
||||
}catch(error2){
|
||||
console.log("error with timestamp: ", cellData);
|
||||
return String(cellData);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
export const TableRowSizeCell = ({ cellData, rowData }) => {
|
||||
const getStringSize = () => {
|
||||
try {
|
||||
// process for getting human readable string from bytes: https://stackoverflow.com/a/18650828
|
||||
let bytes = parseInt(cellData);
|
||||
if (cellData === '' || cellData === undefined) return '';
|
||||
if (bytes === 0) return '0 B';
|
||||
const decimals = 2;
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
} catch (error) {
|
||||
return cellData;
|
||||
}
|
||||
};
|
||||
return getStringSize(cellData);
|
||||
};
|
||||
@@ -20,6 +20,16 @@ import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip'
|
||||
import Split from 'react-split';
|
||||
import {subscriptionCallbackTokens} from "./CallbacksTabsTaskingInput";
|
||||
import {CallbacksTabsTaskingInputTokenSelect} from "./CallbacksTabsTaskingInputTokenSelect";
|
||||
import KeyboardReturnIcon from '@mui/icons-material/KeyboardReturn';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import {b64DecodeUnicode} from "./ResponseDisplay";
|
||||
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
|
||||
import PlaylistRemoveIcon from '@mui/icons-material/PlaylistRemove';
|
||||
import {useGetMythicSetting, useSetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {RenderSingleTask} from "../SingleTaskView/SingleTaskView";
|
||||
import {loadedCommandsQuery} from "./CallbacksTabsProcessBrowser";
|
||||
import {getSkewedNow} from "../../utilities/Time";
|
||||
|
||||
const fileDataFragment = gql`
|
||||
fragment fileObjData on mythictree {
|
||||
@@ -28,6 +38,8 @@ const fileDataFragment = gql`
|
||||
task_id
|
||||
filemeta {
|
||||
id
|
||||
agent_file_id
|
||||
filename_text
|
||||
}
|
||||
tags {
|
||||
tagtype {
|
||||
@@ -35,10 +47,12 @@ const fileDataFragment = gql`
|
||||
color
|
||||
id
|
||||
}
|
||||
id
|
||||
}
|
||||
host
|
||||
id
|
||||
can_have_children
|
||||
has_children
|
||||
success
|
||||
full_path_text
|
||||
name_text
|
||||
@@ -56,20 +70,32 @@ const fileDataFragment = gql`
|
||||
const rootFileQuery = gql`
|
||||
${fileDataFragment}
|
||||
query myRootFolderQuery {
|
||||
mythictree(where: { parent_path_text: { _eq: "" }, tree_type: {_eq: "file"} }) {
|
||||
mythictree(where: { parent_path_text: { _eq: "" }, tree_type: {_eq: "file"} }, order_by: {id: desc}) {
|
||||
...fileObjData
|
||||
}
|
||||
}
|
||||
`;
|
||||
const folderQuery = gql`
|
||||
${fileDataFragment}
|
||||
query myFolderQuery($parent_path_text: String!) {
|
||||
mythictree(
|
||||
query myFolderQuery($parent_path_text: String!, $parents: [String]) {
|
||||
children: mythictree(
|
||||
where: { parent_path_text: { _eq: $parent_path_text }, tree_type: {_eq: "file"} }
|
||||
order_by: { can_have_children: asc, name: asc }
|
||||
) {
|
||||
...fileObjData
|
||||
}
|
||||
parents: mythictree(
|
||||
where: { full_path_text: { _in: $parents }, tree_type: {_eq: "file"} }
|
||||
order_by: { can_have_children: asc, name: asc }
|
||||
) {
|
||||
...fileObjData
|
||||
}
|
||||
self: mythictree(
|
||||
where: { full_path_text: { _eq: $parent_path_text }, tree_type: {_eq: "file"} }
|
||||
order_by: { can_have_children: asc, name: asc }
|
||||
) {
|
||||
...fileObjData
|
||||
}
|
||||
}
|
||||
`;
|
||||
const fileDataSubscription = gql`
|
||||
@@ -84,7 +110,40 @@ const fileDataSubscription = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const fileBrowserTaskSub = gql`
|
||||
subscription fileBrowserTasks($now: timestamp!, $callback_id: Int!){
|
||||
task_stream(
|
||||
batch_size: 10,
|
||||
cursor: {initial_value: {timestamp: $now}},
|
||||
where: {tasking_location: {_eq: "file_browser"}, callback_id: {_eq: $callback_id}}
|
||||
){
|
||||
id
|
||||
status
|
||||
completed
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const getAllParentNodes = (node) => {
|
||||
let linuxPathSeparatorIndex = node.full_path_text.indexOf("/");
|
||||
let windowsPathSeparatorIndex = node.full_path_text.indexOf("\\");
|
||||
let newNodes = [node.full_path_text];
|
||||
if(linuxPathSeparatorIndex !== -1){
|
||||
let pieces = node.full_path_text.split("/");
|
||||
newNodes.push("/");
|
||||
for(let i = 0; i < pieces.length; i++){
|
||||
let newElement = pieces.slice(0, i).join("/");
|
||||
if(newElement === ""){continue}
|
||||
newNodes.push(newElement)
|
||||
}
|
||||
}
|
||||
if(windowsPathSeparatorIndex !== -1){
|
||||
let pieces = node.full_path_text.split("\\");
|
||||
for(let i = 0; i < pieces.length; i++){
|
||||
newNodes.push(pieces.slice(0, i).join("\\"))
|
||||
}
|
||||
}
|
||||
return newNodes;
|
||||
}
|
||||
export function CallbacksTabsFileBrowserLabel(props) {
|
||||
const [description, setDescription] = React.useState('File Browser: ' + props.tabInfo.displayID);
|
||||
const [openEditDescriptionDialog, setOpenEditDescriptionDialog] = React.useState(false);
|
||||
@@ -116,7 +175,7 @@ export function CallbacksTabsFileBrowserLabel(props) {
|
||||
]);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicTabLabel label={description} onDragTab={props.onDragTab} {...props} contextMenuOptions={contextMenuOptions} />
|
||||
<MythicTabLabel label={description} highlight={props.newDataForTab[props.tabInfo.tabID]} onDragTab={props.onDragTab} {...props} contextMenuOptions={contextMenuOptions} />
|
||||
{openEditDescriptionDialog && (
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
@@ -126,7 +185,7 @@ export function CallbacksTabsFileBrowserLabel(props) {
|
||||
}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title={"Edit Tab's Description"}
|
||||
title={"Edit Tab's Description - Displays as one line"}
|
||||
onClose={() => {
|
||||
setOpenEditDescriptionDialog(false);
|
||||
}}
|
||||
@@ -139,27 +198,50 @@ export function CallbacksTabsFileBrowserLabel(props) {
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) => {
|
||||
const [fromNow, setFromNow] = React.useState((new Date()));
|
||||
export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNewDataForTab, baseUIFeature }) => {
|
||||
const fromNow = React.useRef(getSkewedNow());
|
||||
const [backdropOpen, setBackdropOpen] = React.useState(false);
|
||||
const treeRootDataRef = React.useRef({}); // hold all of the actual data
|
||||
const [treeAdjMtx, setTreeAdjMtx] = React.useState({}); // hold the simple adjacency matrix for parent/child relationships
|
||||
// hold the simple adjacency matrix for parent/child relationships
|
||||
const [treeAdjMtx, setTreeAdjMtx] = React.useState({});
|
||||
const [selectedFolderData, setSelectedFolderData] = React.useState({
|
||||
full_path_text: '.',
|
||||
host: "",
|
||||
id: "",
|
||||
full_path_text: '',
|
||||
host: tabInfo.host,
|
||||
group: "Default",
|
||||
parent_path_text: ""
|
||||
});
|
||||
const [showDeletedFiles, setShowDeletedFiles] = React.useState(false);
|
||||
const [openTaskingButton, setOpenTaskingButton] = React.useState(false);
|
||||
const taskingData = React.useRef({"parameters": "", "ui_feature": "file_browser:list"});
|
||||
const taskingTableTopTypedDataRef = React.useRef({
|
||||
path: "",
|
||||
token: 0,
|
||||
});
|
||||
const autoTaskLsOnEmptyDirectoriesRef = React.useRef(false);
|
||||
const taskingData = React.useRef({"parameters": "", "ui_feature": baseUIFeature +":list"});
|
||||
const mountedRef = React.useRef(true);
|
||||
const tableOpenedPathIdRef = React.useRef(0);
|
||||
const tableOpenedPathIdRef = React.useRef({
|
||||
host: "",
|
||||
group: "",
|
||||
full_path_text: ""
|
||||
});
|
||||
const loadedCommandsRef = React.useRef({});
|
||||
const loadingCommandsRef = React.useRef(false);
|
||||
useQuery(rootFileQuery, {
|
||||
onCompleted: (data) => {
|
||||
// use an adjacency matrix but only for full_path_text -> children, not both directions
|
||||
// create the top level data in the treeRootDataRef
|
||||
let defaultGroup = "Default";
|
||||
for(let i = 0; i < data.mythictree.length; i++){
|
||||
let currentGroups = data.mythictree[i]?.["callback"]?.["mythictree_groups"] || ["Unknown Callbacks"];
|
||||
try{
|
||||
if(data.mythictree[i]?.['callback']?.['id'] === tabInfo.callbackID){
|
||||
if(!currentGroups.includes(defaultGroup, 0)){
|
||||
defaultGroup = currentGroups[0];
|
||||
}
|
||||
}
|
||||
}catch(error){
|
||||
console.log(error);
|
||||
}
|
||||
for(let j = 0; j < currentGroups.length; j++){
|
||||
if(treeRootDataRef.current[currentGroups[j]] === undefined){
|
||||
treeRootDataRef.current[currentGroups[j]] = {};
|
||||
@@ -168,7 +250,17 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
|
||||
// new host discovered
|
||||
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]] = {};
|
||||
}
|
||||
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] = {...data.mythictree[i]}
|
||||
if(treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] === undefined){
|
||||
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] = {...data.mythictree[i]};
|
||||
} else {
|
||||
if(treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]].success === null){
|
||||
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] = {...data.mythictree[i]}
|
||||
}
|
||||
}
|
||||
if(data.mythictree[i].id > treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]].id){
|
||||
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] |= data.mythictree[i].has_children;
|
||||
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]].success |= data.mythictree[i].success;
|
||||
}
|
||||
}
|
||||
}
|
||||
// create the top level data in the adjacency matrix
|
||||
@@ -192,11 +284,17 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
|
||||
}, {...treeAdjMtx});
|
||||
//console.log(treeRootDataRef.current);
|
||||
setTreeAdjMtx(newMatrix);
|
||||
setSelectedFolderData({
|
||||
full_path_text: '',
|
||||
host: tabInfo.host,
|
||||
group: defaultGroup,
|
||||
parent_path_text: "",
|
||||
});
|
||||
},
|
||||
fetchPolicy: 'no-cache',
|
||||
});
|
||||
useSubscription(fileDataSubscription, {
|
||||
variables: {now: fromNow},
|
||||
variables: {now: fromNow.current},
|
||||
fetchPolicy: "no-cache",
|
||||
onData: ({data}) => {
|
||||
for(let i = 0; i < data.data.mythictree_stream.length; i++){
|
||||
@@ -209,7 +307,54 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
|
||||
// new host discovered
|
||||
treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]] = {};
|
||||
}
|
||||
treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]] = {...data.data.mythictree_stream[i]};
|
||||
if(treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]] === undefined){
|
||||
// first time we're seeing this file data, just add it
|
||||
treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]] = {...data.data.mythictree_stream[i]};
|
||||
treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]].filemeta = treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]].filemeta.map(f => {
|
||||
return {...f, filename_text: b64DecodeUnicode(f.filename_text)}
|
||||
})
|
||||
if(selectedFolderData.group === currentGroups[j] && selectedFolderData.host === data.data.mythictree_stream[i]["host"] &&
|
||||
selectedFolderData.full_path_text === data.data.mythictree_stream[i]["full_path_text"]){
|
||||
setSelectedFolderData({...treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]],
|
||||
group: currentGroups[j], fromHistory: selectedFolderData.fromHistory});
|
||||
}
|
||||
} else {
|
||||
// we need to merge data in because we already have some info
|
||||
let existingData = treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]];
|
||||
if(existingData.task_id <= data.data.mythictree_stream[i].task_id){
|
||||
existingData.deleted = data.data.mythictree_stream[i].deleted;
|
||||
}
|
||||
if( (existingData.success === null || !existingData.success) && data.data.mythictree_stream[i].success !== null){
|
||||
existingData.success = data.data.mythictree_stream[i].success;
|
||||
existingData.task_id = data.data.mythictree_stream[i].task_id;
|
||||
}
|
||||
existingData.comment = data.data.mythictree_stream[i].comment;
|
||||
existingData.tags = [...existingData.tags, ...data.data.mythictree_stream[i].tags];
|
||||
existingData.has_children = data.data.mythictree_stream[i].has_children || existingData.has_children;
|
||||
existingData.metadata.size = data.data.mythictree_stream[i].metadata.size;
|
||||
existingData.metadata.access_time = data.data.mythictree_stream[i].metadata.access_time;
|
||||
existingData.metadata.modify_time = data.data.mythictree_stream[i].metadata.modify_time;
|
||||
if(data.data.mythictree_stream[i].metadata.permissions !== undefined && data.data.mythictree_stream[i].metadata.permissions !== null){
|
||||
if(Array.isArray(existingData.metadata.permissions) && Array.isArray(data.data.mythictree_stream[i].metadata.permissions)){
|
||||
existingData.metadata.permissions = [...existingData.metadata.permissions, ...data.data.mythictree_stream[i].metadata.permissions];
|
||||
} else if(Array.isArray(existingData.metadata.permissions) !== Array.isArray(data.data.mythictree_stream[i].metadata.permissions)) {
|
||||
// had array/dict and got mismatched array/dict, so just take newer one
|
||||
existingData.metadata.permissions = data.data.mythictree_stream[i].metadata.permissions;
|
||||
} else {
|
||||
existingData.metadata.permissions = {...existingData.metadata.permissions, ...data.data.mythictree_stream[i].metadata.permissions};
|
||||
}
|
||||
}
|
||||
let newfileData = data.data.mythictree_stream[i].filemeta.map(f => {
|
||||
return {...f, filename_text: b64DecodeUnicode(f.filename_text)};
|
||||
})
|
||||
existingData.filemeta = [...existingData.filemeta, ...newfileData]
|
||||
existingData.filemeta.sort((a,b) => a.id > b.id ? 1 : -1);
|
||||
treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]] = {...existingData};
|
||||
if(selectedFolderData.group === currentGroups[j] && selectedFolderData.host === data.data.mythictree_stream[i]["host"] &&
|
||||
selectedFolderData.full_path_text === data.data.mythictree_stream[i]["full_path_text"]){
|
||||
setSelectedFolderData({...existingData, group: currentGroups[j], fromHistory: selectedFolderData.fromHistory});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const newMatrix = data.data.mythictree_stream.reduce( (prev, cur) => {
|
||||
@@ -231,9 +376,57 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
|
||||
return prev;
|
||||
}, {...treeAdjMtx});
|
||||
setTreeAdjMtx(newMatrix);
|
||||
|
||||
if(index !== value){
|
||||
setNewDataForTab((prev) => {return {...prev, [tabInfo.tabID]: true}});
|
||||
}
|
||||
}
|
||||
})
|
||||
useSubscription(fileBrowserTaskSub, {
|
||||
variables: {now: fromNow.current, callback_id: tabInfo.callbackID},
|
||||
fetchPolicy: "no-cache",
|
||||
onData: ({data}) => {
|
||||
for(let i = 0; i < data.data.task_stream.length; i++){
|
||||
if(data.data.task_stream[i].status.toLowerCase().includes("error") && data.data.task_stream[i].completed){
|
||||
snackActions.error(<RenderSingleTask task_id={data.data.task_stream[i].id} />,
|
||||
{toastId: data.data.task_stream[i].id, autoClose: false, closeOnClick: false});
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
const [getLoadedCommandsQuery] = useLazyQuery(loadedCommandsQuery, {
|
||||
fetchPolicy: 'no-cache',
|
||||
onCompleted: (data) => {
|
||||
if(data.loadedcommands.length > 0){
|
||||
loadedCommandsRef.current[data.loadedcommands[0].callback_id] = [...data.loadedcommands];
|
||||
}
|
||||
loadingCommandsRef.current = false;
|
||||
},
|
||||
onError: (data) => {
|
||||
console.log(data);
|
||||
loadingCommandsRef.current = false;
|
||||
}
|
||||
});
|
||||
async function getLoadedCommandForUIFeature (callback_id, uifeature){
|
||||
while(loadingCommandsRef.current){
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
}
|
||||
if(loadedCommandsRef.current[callback_id] === undefined){
|
||||
loadingCommandsRef.current = true;
|
||||
getLoadedCommandsQuery({variables: {callback_id: callback_id}});
|
||||
while(loadingCommandsRef.current){
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
}
|
||||
}
|
||||
if(loadedCommandsRef.current[callback_id] === undefined){
|
||||
return undefined;
|
||||
}
|
||||
for(let i = 0; i < loadedCommandsRef.current[callback_id].length; i++){
|
||||
if(loadedCommandsRef.current[callback_id][i].command.supported_ui_features.includes(uifeature)){
|
||||
return loadedCommandsRef.current[callback_id][i];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
const [getFolderData] = useLazyQuery(folderQuery, {
|
||||
onError: (data) => {
|
||||
console.error(data);
|
||||
@@ -242,23 +435,74 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
|
||||
notifyOnNetworkStatusChange: true,
|
||||
onCompleted: (data) => {
|
||||
//console.log("getFolderData", data)
|
||||
let mythictree = [...data.parents, ...data.children];
|
||||
snackActions.dismiss();
|
||||
// add in all of the raw data
|
||||
for(let i = 0; i < data.mythictree.length; i++){
|
||||
let currentGroups = data.mythictree[i]?.["callback"]?.["mythictree_groups"] || ["Unknown Callbacks"];
|
||||
for(let i = 0; i < mythictree.length; i++){
|
||||
let currentGroups = mythictree[i]?.["callback"]?.["mythictree_groups"] || ["Unknown Callbacks"];
|
||||
for(let j = 0; j < currentGroups.length; j++){
|
||||
if(treeRootDataRef.current[currentGroups[j]] === undefined){
|
||||
treeRootDataRef.current[currentGroups[j]] = {};
|
||||
}
|
||||
if( treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]] === undefined) {
|
||||
if( treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]] === undefined) {
|
||||
// new host discovered
|
||||
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]] = {};
|
||||
treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]] = {};
|
||||
}
|
||||
if(treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]][mythictree[i]["full_path_text"]] === undefined){
|
||||
// first time we're seeing this file data, just add it
|
||||
treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]][mythictree[i]["full_path_text"]] = {...mythictree[i]};
|
||||
treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]][mythictree[i]["full_path_text"]].filemeta = treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]][mythictree[i]["full_path_text"]].filemeta.map(f => {
|
||||
return {...f, filename_text: b64DecodeUnicode(f.filename_text)}
|
||||
})
|
||||
if(selectedFolderData.group === currentGroups[j] && selectedFolderData.host === mythictree[i]["host"] &&
|
||||
selectedFolderData.full_path_text === mythictree[i]["full_path_text"]){
|
||||
setSelectedFolderData({...treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]][mythictree[i]["full_path_text"]],
|
||||
group: currentGroups[j], fromHistory: selectedFolderData.fromHistory});
|
||||
}
|
||||
} else {
|
||||
// we need to merge data in because we already have some info
|
||||
let existingData = treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]][mythictree[i]["full_path_text"]];
|
||||
if(existingData.task_id <= mythictree[i].task_id){
|
||||
existingData.deleted = mythictree[i].deleted;
|
||||
}
|
||||
if( (existingData.success === null || !existingData.success) && mythictree[i].success !== null){
|
||||
existingData.success = mythictree[i].success;
|
||||
existingData.task_id = mythictree[i].task_id;
|
||||
}
|
||||
existingData.comment += mythictree[i].comment;
|
||||
existingData.tags = [...existingData.tags, ...mythictree[i].tags];
|
||||
existingData.has_children = mythictree[i].has_children || existingData.has_children;
|
||||
existingData.metadata.size = mythictree[i].metadata.size;
|
||||
existingData.metadata.access_time = mythictree[i].metadata.access_time;
|
||||
existingData.metadata.modify_time = mythictree[i].metadata.modify_time;
|
||||
//console.log("updating permissions", "existing", existingData.metadata.permissions, "new", mythictree[i].metadata)
|
||||
if(mythictree[i].metadata.permissions !== undefined && mythictree[i].metadata.permissions !== null){
|
||||
if(Array.isArray(existingData.metadata.permissions) && Array.isArray(mythictree[i].metadata.permissions)){
|
||||
existingData.metadata.permissions = [...existingData.metadata.permissions, ...mythictree[i].metadata.permissions];
|
||||
} else if(Array.isArray(existingData.metadata.permissions) !== Array.isArray(mythictree[i].metadata.permissions)) {
|
||||
// had array/dict and got mismatched array/dict, so just take newer one
|
||||
existingData.metadata.permissions = mythictree[i].metadata.permissions;
|
||||
} else {
|
||||
existingData.metadata.permissions = {...existingData.metadata.permissions, ...mythictree[i].metadata.permissions};
|
||||
}
|
||||
}
|
||||
let newfileData = mythictree[i].filemeta.map(f => {
|
||||
return {...f, filename_text: b64DecodeUnicode(f.filename_text)};
|
||||
})
|
||||
existingData.filemeta = [...existingData.filemeta, ...newfileData]
|
||||
existingData.filemeta.sort((a,b) => a.id > b.id ? 1 : -1);
|
||||
treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]][mythictree[i]["full_path_text"]] = {...existingData};
|
||||
if(selectedFolderData.group === currentGroups[j] && selectedFolderData.host === mythictree[i]["host"] &&
|
||||
selectedFolderData.full_path_text === mythictree[i]["full_path_text"]){
|
||||
setSelectedFolderData({...existingData,
|
||||
group: currentGroups[j],
|
||||
fromHistory: selectedFolderData.fromHistory});
|
||||
}
|
||||
}
|
||||
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] = {...data.mythictree[i]}
|
||||
}
|
||||
}
|
||||
// create the top level data in the adjacency matrix
|
||||
const newMatrix = data.mythictree.reduce( (prev, cur) => {
|
||||
const newMatrix = mythictree.reduce( (prev, cur) => {
|
||||
let currentGroups = cur?.["callback"]?.["mythictree_groups"] || ["Unknown Callbacks"];
|
||||
for(let j = 0; j < currentGroups.length; j++) {
|
||||
if (prev[currentGroups[j]] === undefined) {
|
||||
@@ -280,41 +524,121 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
|
||||
setTreeAdjMtx(newMatrix);
|
||||
//console.log("just set treeAdjMtx, about to close backdrop")
|
||||
setBackdropOpen(false);
|
||||
|
||||
if(data.self.length > 0){
|
||||
//setSelectedFolderData({...selectedFolderData, task_id: data.self[0].task_id, success: data.self[0].success});
|
||||
// this path exists, let's see if we have data for it and if the user wants us to auto-issue an ls for the path
|
||||
let newAllData = Object.keys(newMatrix[selectedFolderData.group]?.[selectedFolderData.host]?.[data.self[0].full_path_text] || {});
|
||||
if(autoTaskLsOnEmptyDirectoriesRef.current){
|
||||
if(newAllData.length === 0 && data.self[0].full_path_text !== "" && data.self[0].success === null){
|
||||
onListFilesButtonFromTableWithNoEntries()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// we couldn't find the path specified, so we must not have data for it, so check if the user wants us to auto issue an ls
|
||||
if(autoTaskLsOnEmptyDirectoriesRef.current){
|
||||
// don't want to auto ls when we get to the root/host object in the view
|
||||
if(selectedFolderData.full_path_text !== ""){
|
||||
onListFilesButtonFromTableWithNoEntries()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
const onSetTableData = useCallback((nodeData) => {
|
||||
//console.log("setTableData", nodeData);
|
||||
setSelectedFolderData(nodeData);
|
||||
}, []);
|
||||
const fetchFolderData = useCallback((nodeData) => {
|
||||
const fetchFolderData = useCallback((nodeData, fromHistory) => {
|
||||
let parentNodes = getAllParentNodes(nodeData);
|
||||
getFolderData({
|
||||
variables: { parent_path_text: nodeData.full_path_text},
|
||||
variables: { parent_path_text: nodeData.full_path_text, parents: parentNodes},
|
||||
});
|
||||
setBackdropOpen(true);
|
||||
tableOpenedPathIdRef.current = nodeData.id;
|
||||
setSelectedFolderData(nodeData);
|
||||
setSelectedFolderData({...nodeData, fromHistory: fromHistory});
|
||||
}, []);
|
||||
const fetchParentFolderData = (nodeData) => {
|
||||
let parentNodes = getAllParentNodes(nodeData);
|
||||
getFolderData({
|
||||
variables: { parent_path_text: nodeData.parent_path_text, parents: parentNodes},
|
||||
});
|
||||
setBackdropOpen(true);
|
||||
let parentData = treeRootDataRef.current[nodeData.group][nodeData.host][nodeData.parent_path_text];
|
||||
parentData.group = nodeData.group;
|
||||
tableOpenedPathIdRef.current = {
|
||||
group: parentData.group,
|
||||
host: parentData.host,
|
||||
full_path_text: parentData.full_path_text
|
||||
};
|
||||
setSelectedFolderData({...parentData, fromHistory: false});
|
||||
};
|
||||
const localSelectedToken = React.useRef("");
|
||||
const onChangeSelectedToken = (token) => {
|
||||
localSelectedToken.current = token;
|
||||
}
|
||||
const onListFilesButton = ({ fullPath, callback_id, callback_display_id, token }) => {
|
||||
let path = fullPath;
|
||||
if(path === ""){
|
||||
path = ".";
|
||||
}
|
||||
taskingData.current = ({
|
||||
"token": token,
|
||||
"parameters": {path: fullPath, full_path: fullPath, host: selectedFolderData.host, file: ""},
|
||||
"ui_feature": "file_browser:list", callback_id, callback_display_id});
|
||||
"parameters": {path: path, full_path: path, host: selectedFolderData.host, file: ""},
|
||||
"ui_feature": baseUIFeature + ":list", callback_id, callback_display_id});
|
||||
setOpenTaskingButton(true);
|
||||
};
|
||||
const onListFilesButtonFromTableWithNoEntries = () => {
|
||||
taskingData.current = ({
|
||||
"token": taskingTableTopTypedDataRef.current.token === "Default Token" ? 0 : taskingTableTopTypedDataRef.current.token,
|
||||
"parameters": {
|
||||
path: taskingTableTopTypedDataRef.current.path,
|
||||
full_path: taskingTableTopTypedDataRef.current.path,
|
||||
host: selectedFolderData.host,
|
||||
file: ""
|
||||
},
|
||||
"ui_feature": baseUIFeature +":list",
|
||||
callback_id: tabInfo.callbackID,
|
||||
callback_display_id: tabInfo.displayID});
|
||||
setOpenTaskingButton(true);
|
||||
}
|
||||
const onUploadFileButton = ({ fullPath, callback_id, callback_display_id, token }) => {
|
||||
taskingData.current = ({
|
||||
"token": token,
|
||||
"parameters": {path: fullPath, full_path: fullPath, host: selectedFolderData.host},
|
||||
"ui_feature": "file_browser:upload", "openDialog": true,
|
||||
"ui_feature": baseUIFeature + ":upload", "openDialog": true,
|
||||
callback_id, callback_display_id
|
||||
});
|
||||
setOpenTaskingButton(true);
|
||||
};
|
||||
const multipleTasks = React.useRef([]);
|
||||
const onTaskRowActions = useCallback( (tasks) => {
|
||||
let updatedTasks = [];
|
||||
for(let i = 0; i < tasks.length; i++){
|
||||
updatedTasks.push({
|
||||
"token": localSelectedToken.current,
|
||||
"parameters": {
|
||||
host: selectedFolderData.host,
|
||||
path: tasks[i].path,
|
||||
full_path: tasks[i].full_path,
|
||||
file: tasks[i].filename,
|
||||
},
|
||||
"ui_feature": tasks[i].uifeature,
|
||||
openDialog: tasks[i].openDialog,
|
||||
getConfirmation: tasks[i].getConfirmation,
|
||||
callback_id: tasks[i].callback_id,
|
||||
callback_display_id: tasks[i].callback_display_id});
|
||||
}
|
||||
multipleTasks.current = updatedTasks;
|
||||
taskingData.current = multipleTasks.current.pop()
|
||||
setOpenTaskingButton(true);
|
||||
}, [selectedFolderData, localSelectedToken.current]);
|
||||
React.useEffect( () => {
|
||||
if(!openTaskingButton){
|
||||
if(multipleTasks.current.length > 0){
|
||||
taskingData.current = multipleTasks.current.pop()
|
||||
setOpenTaskingButton(true);
|
||||
}
|
||||
}
|
||||
}, [openTaskingButton]);
|
||||
const onTaskRowAction = useCallback(({ path, full_path, filename, uifeature, openDialog, getConfirmation, callback_id, callback_display_id }) => {
|
||||
taskingData.current = ({
|
||||
"token": localSelectedToken.current,
|
||||
@@ -339,13 +663,26 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
|
||||
const taskListing = (nodeData, callback_id, callback_display_id) => {
|
||||
taskingData.current = ({
|
||||
"token": localSelectedToken.current,
|
||||
"parameters": {path: nodeData.full_path_text, full_path: nodeData.full_path_text, host: selectedFolderData.host, file: ""},
|
||||
"ui_feature": "file_browser:list", callback_id, callback_display_id});
|
||||
"parameters": {path: nodeData.full_path_text, full_path: nodeData.full_path_text, host: nodeData.host, file: ""},
|
||||
"ui_feature": baseUIFeature + ":list", callback_id, callback_display_id});
|
||||
setOpenTaskingButton(true);
|
||||
}
|
||||
const openDirectoryPath = ({group, host, path}) => {
|
||||
let parentNodes = getAllParentNodes({full_path_text: path});
|
||||
getFolderData({
|
||||
variables: {parent_path_text: path, parents: parentNodes}
|
||||
})
|
||||
setBackdropOpen(true);
|
||||
setSelectedFolderData({host, group, full_path_text: path, id: 0, success: null});
|
||||
tableOpenedPathIdRef.current = {
|
||||
group: group,
|
||||
host: host,
|
||||
full_path_text: path
|
||||
};
|
||||
}
|
||||
return (
|
||||
<MythicTabPanel index={index} value={value}>
|
||||
<Split direction="horizontal" style={{width: "100%", height: "100%", display: "flex"}} sizes={[30, 70]} >
|
||||
<Split direction="horizontal" style={{width: "100%", height: "100%", display: "flex", overflow: "hidden"}} sizes={[30, 70]} >
|
||||
<div className="bg-gray-base" style={{display: "inline-flex"}}>
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={true}>
|
||||
<CircularProgress color="inherit" />
|
||||
@@ -355,23 +692,32 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
|
||||
showDeletedFiles={showDeletedFiles}
|
||||
treeRootData={treeRootDataRef.current}
|
||||
treeAdjMatrix={treeAdjMtx}
|
||||
selectedFolderData={selectedFolderData}
|
||||
fetchFolderData={fetchFolderData}
|
||||
setTableData={onSetTableData}
|
||||
taskListing={taskListing}
|
||||
tableOpenedPathId={tableOpenedPathIdRef.current}
|
||||
getLoadedCommandForUIFeature={getLoadedCommandForUIFeature}
|
||||
baseUIFeature={baseUIFeature}
|
||||
/>
|
||||
|
||||
</div>
|
||||
<div className="bg-gray-light" style={{display: "inline-flex"}}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', flexGrow: 1, overflow: "hidden" }}>
|
||||
<div style={{ flexGrow: 0 }}>
|
||||
<FileBrowserTableTop
|
||||
tabInfo={tabInfo}
|
||||
taskingTableTopTypedDataRef={taskingTableTopTypedDataRef}
|
||||
autoTaskLsOnEmptyDirectoriesRef={autoTaskLsOnEmptyDirectoriesRef}
|
||||
onChangeSelectedToken={onChangeSelectedToken}
|
||||
selectedFolderData={selectedFolderData}
|
||||
onListFilesButton={onListFilesButton}
|
||||
onUploadFileButton={onUploadFileButton}
|
||||
toggleShowDeletedFiles={toggleShowDeletedFiles}
|
||||
fetchParentFolderData={fetchParentFolderData}
|
||||
fetchFolderData={fetchFolderData}
|
||||
openDirectoryPath={openDirectoryPath}
|
||||
baseUIFeature={baseUIFeature}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flexGrow: 1 }}>
|
||||
@@ -384,9 +730,14 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
|
||||
onRowDoubleClick={fetchFolderData}
|
||||
treeRootData={treeRootDataRef.current}
|
||||
treeAdjMatrix={treeAdjMtx}
|
||||
onListFilesButtonFromTableWithNoEntries={onListFilesButtonFromTableWithNoEntries}
|
||||
selectedFolderData={selectedFolderData}
|
||||
autoTaskLsOnEmptyDirectories={autoTaskLsOnEmptyDirectoriesRef.current}
|
||||
onTaskRowAction={onTaskRowAction}
|
||||
onTaskRowActions={onTaskRowActions}
|
||||
getLoadedCommandForUIFeature={getLoadedCommandForUIFeature}
|
||||
me={me}
|
||||
baseUIFeature={baseUIFeature}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -409,28 +760,45 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
|
||||
};
|
||||
const FileBrowserTableTop = ({
|
||||
selectedFolderData,
|
||||
taskingTableTopTypedDataRef,
|
||||
autoTaskLsOnEmptyDirectoriesRef,
|
||||
onChangeSelectedToken,
|
||||
onListFilesButton,
|
||||
onUploadFileButton,
|
||||
toggleShowDeletedFiles,
|
||||
tabInfo
|
||||
fetchParentFolderData,
|
||||
fetchFolderData,
|
||||
openDirectoryPath,
|
||||
tabInfo,
|
||||
baseUIFeature
|
||||
}) => {
|
||||
const autoTaskLsOnEmptyDirectories = useGetMythicSetting({
|
||||
setting_name: "autoTaskLsOnEmptyDirectories", default_value: false
|
||||
});
|
||||
const [updateMythicSetting] = useSetMythicSetting();
|
||||
const [fullPath, setFullPath] = React.useState('');
|
||||
const selectedToken = React.useRef("Default Token");
|
||||
const [tokenOptions, setTokenOptions] = React.useState([]);
|
||||
const [placeHolder, setPlaceHolder] = React.useState(selectedFolderData.host);
|
||||
const [showDeletedFiles, setLocalShowDeletedFiles] = React.useState(false);
|
||||
const [history, setHistory] = React.useState([]);
|
||||
const [historyIndex, setHistoryIndex] = React.useState(0);
|
||||
const onChangePath = (_, value) => {
|
||||
setFullPath(value);
|
||||
taskingTableTopTypedDataRef.current.path = value;
|
||||
taskingTableTopTypedDataRef.current.token = selectedToken;
|
||||
|
||||
};
|
||||
const changeSelectedToken = (token) => {
|
||||
onChangeSelectedToken(token);
|
||||
if(token === "Default Token"){
|
||||
selectedToken.current = "Default Token";
|
||||
taskingTableTopTypedDataRef.current.token = selectedToken;
|
||||
return;
|
||||
}
|
||||
if(token.token_id !== selectedToken.current.token_id){
|
||||
selectedToken.current = token;
|
||||
taskingTableTopTypedDataRef.current.token = selectedToken;
|
||||
}
|
||||
}
|
||||
useSubscription(subscriptionCallbackTokens, {
|
||||
@@ -438,11 +806,18 @@ const FileBrowserTableTop = ({
|
||||
shouldResubscribe: true,
|
||||
onData: ({data}) => {
|
||||
setTokenOptions(data.data.callbacktoken);
|
||||
if(data.data.callbacktoken.length === 0) {
|
||||
onChangeSelectedToken("Default Token");
|
||||
}
|
||||
}
|
||||
});
|
||||
useEffect(() => {
|
||||
if (selectedFolderData.full_path_text !== undefined) {
|
||||
if(selectedFolderData.root) {
|
||||
setFullPath("");
|
||||
taskingTableTopTypedDataRef.current.path = "";
|
||||
} else if (selectedFolderData.full_path_text !== undefined) {
|
||||
setFullPath(selectedFolderData.full_path_text);
|
||||
taskingTableTopTypedDataRef.current.path = selectedFolderData.full_path_text;
|
||||
}
|
||||
const groups = selectedFolderData?.callback?.mythictree_groups?.join(", ") || "";
|
||||
if(groups.length > 0 ){
|
||||
@@ -450,14 +825,53 @@ const FileBrowserTableTop = ({
|
||||
} else {
|
||||
setPlaceHolder(selectedFolderData.host + groups);
|
||||
}
|
||||
|
||||
}, [selectedFolderData]);
|
||||
const onLocalListFilesButton = () => {
|
||||
if (fullPath === '') {
|
||||
snackActions.warning('Must provide a path to list');
|
||||
useEffect( () => {
|
||||
if(selectedFolderData.fromHistory){
|
||||
return;
|
||||
}
|
||||
onListFilesButton({ fullPath, token: selectedToken.current });
|
||||
if(selectedFolderData.id !== ""){
|
||||
if(history[0]?.full_path_text !== selectedFolderData.full_path_text){
|
||||
// always add newest things to the bottom of the stack
|
||||
setHistory([selectedFolderData, ...history]);
|
||||
if(history.length > 20){
|
||||
// pop off from the top (the oldest) if we get more than 50
|
||||
setHistory([selectedFolderData, ...history.splice(history.length-1, 1)])
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [selectedFolderData, history])
|
||||
const moveIndexToPreviousListing = () => {
|
||||
// we're getting closer to the end of the historyRef.current, the oldest listing
|
||||
if(historyIndex >= history.length -1){
|
||||
return
|
||||
}
|
||||
setHistoryIndex(historyIndex + 1);
|
||||
fetchFolderData(history[historyIndex + 1], true);
|
||||
}
|
||||
const moveIndexToNextListing = () => {
|
||||
// we're getting close to index 0, the newest listing
|
||||
//console.log(historyIndex, history);
|
||||
if(historyIndex <= 0){
|
||||
return
|
||||
}
|
||||
setHistoryIndex(historyIndex - 1);
|
||||
fetchFolderData(history[historyIndex - 1], true);
|
||||
}
|
||||
const onLocalListFilesButton = () => {
|
||||
let tempPath = fullPath;
|
||||
if (tempPath === "") {
|
||||
tempPath = ".";
|
||||
}
|
||||
if(tempPath.length > 1){
|
||||
if(tempPath[tempPath.length-1] === "/" || tempPath[tempPath.length-1] === "\\"){
|
||||
let newFullPath = tempPath.slice(0, tempPath.length-1);
|
||||
setFullPath(newFullPath);
|
||||
onListFilesButton({ fullPath: newFullPath, token: selectedToken.current });
|
||||
return
|
||||
}
|
||||
}
|
||||
onListFilesButton({ fullPath: tempPath, token: selectedToken.current });
|
||||
};
|
||||
const onLocalUploadFileButton = () => {
|
||||
onUploadFileButton({ fullPath, token: selectedToken.current });
|
||||
@@ -466,28 +880,85 @@ const FileBrowserTableTop = ({
|
||||
setLocalShowDeletedFiles(!showDeletedFiles);
|
||||
toggleShowDeletedFiles(!showDeletedFiles);
|
||||
};
|
||||
const onLocalMoveUpDirectoryButton = () => {
|
||||
if(selectedFolderData.parent_path_text !== ""){
|
||||
fetchParentFolderData(selectedFolderData)
|
||||
}
|
||||
};
|
||||
const onToggleAutoTaskLsOnEmptyDirectories = () => {
|
||||
updateMythicSetting({setting_name: "autoTaskLsOnEmptyDirectories", value: !autoTaskLsOnEmptyDirectories});
|
||||
if(autoTaskLsOnEmptyDirectories){
|
||||
snackActions.info("No longer auto issuing listings for empty paths");
|
||||
} else {
|
||||
snackActions.success("Now starting to auto issue listings for empty paths");
|
||||
}
|
||||
}
|
||||
const goToDirectory = () => {
|
||||
if(fullPath === ""){return}
|
||||
if(fullPath.length > 1){
|
||||
if(fullPath[fullPath.length-1] === "/" || fullPath[fullPath.length-1] === "\\"){
|
||||
let newFullPath = fullPath.slice(0, fullPath.length-1);
|
||||
setFullPath(newFullPath);
|
||||
openDirectoryPath({
|
||||
group: selectedFolderData.group,
|
||||
host: selectedFolderData.host,
|
||||
path: newFullPath,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
openDirectoryPath({
|
||||
group:selectedFolderData.group,
|
||||
host: selectedFolderData.host,
|
||||
path: fullPath,
|
||||
})
|
||||
}
|
||||
useEffect( () => {
|
||||
autoTaskLsOnEmptyDirectoriesRef.current = autoTaskLsOnEmptyDirectories;
|
||||
}, [autoTaskLsOnEmptyDirectories]);
|
||||
return (
|
||||
<Grid container spacing={0} style={{ paddingTop: '10px' }}>
|
||||
<Grid item xs={12}>
|
||||
<Grid size={12}>
|
||||
<MythicTextField
|
||||
placeholder={selectedFolderData.host}
|
||||
value={fullPath}
|
||||
onEnter={onLocalListFilesButton}
|
||||
onEnter={goToDirectory}
|
||||
onChange={onChangePath}
|
||||
name={placeHolder}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<React.Fragment>
|
||||
<MythicStyledTooltip title={`Task current callback (${tabInfo["displayID"]}) to list contents`}>
|
||||
<IconButton style={{ padding: '3px' }} onClick={onLocalListFilesButton} size="large">
|
||||
<IconButton style={{ padding: '3px' }}
|
||||
onClick={onLocalListFilesButton}
|
||||
size="large">
|
||||
<RefreshIcon color='info' />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={`Upload file to folder via current callback (${tabInfo["displayID"]})`}>
|
||||
<IconButton style={{ padding: '3px' }} onClick={onLocalUploadFileButton} size="large">
|
||||
<IconButton style={{ padding: '3px' }}
|
||||
onClick={onLocalUploadFileButton}
|
||||
size="large">
|
||||
<CloudUploadIcon color="info" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
{autoTaskLsOnEmptyDirectories ? (
|
||||
<MythicStyledTooltip title={"Currently tasking listing on empty directories, click to toggle off"} >
|
||||
<IconButton style={{padding: "3px"}}
|
||||
onClick={onToggleAutoTaskLsOnEmptyDirectories}
|
||||
size={"large"}>
|
||||
<PlaylistAddIcon color={"success"} ></PlaylistAddIcon>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
) : (
|
||||
<MythicStyledTooltip title={"Currently not tasking listing on empty directories, click to toggle on"} >
|
||||
<IconButton style={{padding: "3px"}}
|
||||
onClick={onToggleAutoTaskLsOnEmptyDirectories}
|
||||
size={"large"}>
|
||||
<PlaylistRemoveIcon color={"secondary"} ></PlaylistRemoveIcon>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
)}
|
||||
<MythicStyledTooltip title={showDeletedFiles ? 'Hide Deleted Files' : 'Show Deleted Files'}>
|
||||
<IconButton
|
||||
style={{ padding: '3px' }}
|
||||
@@ -503,12 +974,44 @@ const FileBrowserTableTop = ({
|
||||
</React.Fragment>
|
||||
),
|
||||
startAdornment: (
|
||||
<React.Fragment>
|
||||
{tokenOptions.length > 0 ? (
|
||||
<CallbacksTabsTaskingInputTokenSelect options={tokenOptions} changeSelectedToken={changeSelectedToken}/>
|
||||
) : null}
|
||||
</React.Fragment>),
|
||||
style: { padding: 0 },
|
||||
<div style={{display: "inline-flex",
|
||||
alignItems: "center",
|
||||
borderRight: "1px solid grey",
|
||||
marginRight: "10px",
|
||||
padding: "0 5px 0 0"}}>
|
||||
{tokenOptions.length > 0 &&
|
||||
<CallbacksTabsTaskingInputTokenSelect width={"100%"}
|
||||
options={tokenOptions} changeSelectedToken={changeSelectedToken}/>
|
||||
}
|
||||
<MythicStyledTooltip title={`Move back to previous listing`}>
|
||||
<IconButton style={{ padding: '3px' }}
|
||||
disabled={historyIndex >= history.length -1 }
|
||||
onClick={moveIndexToPreviousListing}
|
||||
color='info'
|
||||
size="large">
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={`Move to next listing`}>
|
||||
<IconButton style={{ padding: '3px' }}
|
||||
disabled={historyIndex <= 0}
|
||||
onClick={moveIndexToNextListing}
|
||||
size="large"
|
||||
color='info'>
|
||||
<ArrowForwardIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Move up a directory"} >
|
||||
<IconButton style={{padding: "0 0 0 0"}}
|
||||
onClick={onLocalMoveUpDirectoryButton}
|
||||
disabled={!selectedFolderData?.parent_path_text || selectedFolderData?.parent_path_text?.length === 0 || selectedFolderData.root || fullPath === ""}
|
||||
>
|
||||
<KeyboardReturnIcon style={{rotate: "90deg"}} ></KeyboardReturnIcon>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
|
||||
</div>),
|
||||
style: { },
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,54 +1,176 @@
|
||||
import React from 'react';
|
||||
import FileBrowserVirtualTree from '../../MythicComponents/MythicFileBrowserVirtualTree';
|
||||
import {FileBrowserVirtualTree} from '../../MythicComponents/MythicFileBrowserVirtualTree';
|
||||
import {MythicDialog} from "../../MythicComponents/MythicDialog";
|
||||
import {ViewCallbackMythicTreeGroupsDialog} from "./ViewCallbackMythicTreeGroupsDialog";
|
||||
import ListIcon from '@mui/icons-material/List';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import {getAllParentNodes} from "./CallbacksTabsFileBrowser";
|
||||
import {Dropdown, DropdownMenuItem, DropdownNestedMenuItem} from "../../MythicComponents/MythicNestedMenus";
|
||||
import {copyStringToClipboard} from "../../utilities/Clipboard";
|
||||
import FileCopyOutlinedIcon from '@mui/icons-material/FileCopyOutlined';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
|
||||
|
||||
export const CallbacksTabsFileBrowserTree = ({ treeRootData, treeAdjMatrix, fetchFolderData, setTableData, taskListing, tableOpenedPathId, showDeletedFiles, tabInfo}) => {
|
||||
export const getOpenIDFromNode = (node) => {
|
||||
return `${node.group};${node.host};${node.full_path_text}`;
|
||||
}
|
||||
export const CallbacksTabsFileBrowserTreePreMemo = ({ treeRootData, treeAdjMatrix, fetchFolderData, setTableData, taskListing, tableOpenedPathId, showDeletedFiles, tabInfo, selectedFolderData, baseUIFeature}) => {
|
||||
const [openNodes, setOpenNodes] = React.useState({});
|
||||
const groupName = React.useRef("");
|
||||
const [openViewGroupsDialog, setOpenViewGroupDialog] = React.useState(false);
|
||||
const lastOpenedNodeRef = React.useRef({group: "", host: "", full_path_text: ""});
|
||||
const toggleNodeExpanded = (nodeId, nodeData) => {
|
||||
//console.log("toggleNodeExpanded", nodeId, nodeData);
|
||||
let lastOpenedNodeNewState = true;
|
||||
let lastOpenedNodeID = getOpenIDFromNode(lastOpenedNodeRef.current);
|
||||
if(!lastOpenedNodeRef.current?.has_children){
|
||||
lastOpenedNodeNewState = false;
|
||||
}
|
||||
lastOpenedNodeRef.current = {...nodeData};
|
||||
setTableData(nodeData);
|
||||
fetchFolderData(nodeData);
|
||||
setOpenNodes({
|
||||
...openNodes,
|
||||
[nodeId]: true
|
||||
[getOpenIDFromNode(nodeData)]: true,
|
||||
//[lastOpenedNodeID]: lastOpenedNodeNewState
|
||||
});
|
||||
};
|
||||
const toggleNodeCollapsed = (nodeId, nodeData) => {
|
||||
const toggleNodeCollapsed = (nodeId, nodeData) => {
|
||||
setOpenNodes({
|
||||
...openNodes,
|
||||
[nodeId]: false
|
||||
[getOpenIDFromNode(nodeData)]: false
|
||||
});
|
||||
};
|
||||
const onSelectNode = (nodeId, nodeData) => {
|
||||
const onSelectNode = React.useCallback( (nodeId, nodeData) => {
|
||||
if(nodeData.root){
|
||||
|
||||
setTableData(nodeData);
|
||||
}else if(nodeData.is_group){
|
||||
groupName.current = nodeData.group;
|
||||
setOpenViewGroupDialog(true);
|
||||
}else {
|
||||
//console.log(nodeData);
|
||||
setTableData(nodeData);
|
||||
//setTableData(nodeData);
|
||||
toggleNodeExpanded(nodeId, nodeData);
|
||||
}
|
||||
|
||||
};
|
||||
}, [openNodes, setTableData]);
|
||||
React.useEffect( () => {
|
||||
let group = tableOpenedPathId.group;
|
||||
let host = tableOpenedPathId.host;
|
||||
let allPaths = [...getAllParentNodes(tableOpenedPathId),];
|
||||
const additionalOpenNodes = allPaths.reduce( (prev, cur) => {
|
||||
return {...prev, [getOpenIDFromNode({group: group, host: host, full_path_text: cur})]: true}
|
||||
}, {});
|
||||
setOpenNodes({
|
||||
...openNodes,
|
||||
[tableOpenedPathId]: true
|
||||
[getOpenIDFromNode(tableOpenedPathId)]: true,
|
||||
...additionalOpenNodes
|
||||
});
|
||||
}, [tableOpenedPathId]);
|
||||
const contextMenuOptions = [
|
||||
React.useEffect( () => {
|
||||
let group = selectedFolderData.group;
|
||||
let host = selectedFolderData.host;
|
||||
let allPaths = [...getAllParentNodes(selectedFolderData)];
|
||||
const additionalOpenNodes = allPaths.reduce( (prev, cur) => {
|
||||
return {...prev, [getOpenIDFromNode({group: group, host: host, full_path_text: cur})]: true}
|
||||
}, {});
|
||||
setOpenNodes({
|
||||
...openNodes,
|
||||
[getOpenIDFromNode(selectedFolderData)]: true,
|
||||
...additionalOpenNodes
|
||||
});
|
||||
}, [selectedFolderData]);
|
||||
const contextMenuOptions= (callback_id, callback_display_id, node) => [
|
||||
{
|
||||
name: "Copy to Clipboard", icon: null, click: () => {}, type: "menu",
|
||||
menuItems: [
|
||||
{
|
||||
name: 'Name', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(node.name_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Full Path', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(node.full_path_text)){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Metadata', type: "item",
|
||||
icon: <FileCopyOutlinedIcon style={{ paddingRight: '5px' }} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
if(copyStringToClipboard(JSON.stringify(node?.metadata, null, 2))){
|
||||
snackActions.success("Copied to clipboard");
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Task Listing',
|
||||
click: ({event, node, callback_id, callback_display_id}) => {
|
||||
name: 'List', type: "item", icon: <ListIcon color="warning" style={{ paddingRight: '5px'}} />,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
taskListing(node, callback_id, callback_display_id);
|
||||
}
|
||||
},
|
||||
];
|
||||
const [openContextMenu, setOpenContextMenu] = React.useState(false);
|
||||
const contextMenuData = React.useRef({options: []});
|
||||
const onContextMenu = React.useCallback( ({event, item, itemTreeData}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
contextMenuData.current = {item, itemTreeData,
|
||||
x: event.clientX,
|
||||
y:event.clientY,
|
||||
dropdownAnchorRef: event.currentTarget};
|
||||
if(contextMenuData.current.item?.root){
|
||||
|
||||
}else if(contextMenuData.current.item?.is_group){
|
||||
|
||||
}else {
|
||||
let options = [{
|
||||
name: contextMenuData.current?.item?.name_text, icon: null, disabled: true, type: "item",
|
||||
click: () => {}
|
||||
}];
|
||||
options.push(...contextMenuOptions(tabInfo["callbackID"], tabInfo["displayID"], contextMenuData.current?.item));
|
||||
if(contextMenuData.current?.item?.callback?.["display_id"] !== tabInfo["displayID"]){
|
||||
options.push({
|
||||
name: "Original Callback: " + contextMenuData.current?.item?.callback?.["display_id"],
|
||||
icon: null, click: () => {}, type: "menu",
|
||||
menuItems: [
|
||||
...contextMenuOptions(contextMenuData.current?.item?.callback?.id, contextMenuData.current?.item?.callback?.display_id, contextMenuData.current?.item)
|
||||
]
|
||||
})
|
||||
}
|
||||
contextMenuData.current.options = options;
|
||||
setOpenContextMenu(true);
|
||||
}
|
||||
}, []);
|
||||
const handleMenuItemClick = (event, click) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
click({event});
|
||||
//contextMenuOptions[index].click({event,
|
||||
// node: {...contextMenuData.current?.item, group: contextMenuData.current?.itemTreeData?.group, host: contextMenuData.current?.itemTreeData?.host},
|
||||
// callback_id, callback_display_id
|
||||
//});
|
||||
setOpenContextMenu(false);
|
||||
};
|
||||
const handleClose = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenContextMenu(false);
|
||||
};
|
||||
//console.log(selectedFolderData)
|
||||
return(
|
||||
<>
|
||||
<FileBrowserVirtualTree
|
||||
@@ -56,11 +178,13 @@ export const CallbacksTabsFileBrowserTree = ({ treeRootData, treeAdjMatrix, fetc
|
||||
treeRootData={treeRootData}
|
||||
treeAdjMatrix={treeAdjMatrix}
|
||||
openNodes={openNodes}
|
||||
selectedFolderData={selectedFolderData}
|
||||
onSelectNode={onSelectNode}
|
||||
onExpandNode={toggleNodeExpanded}
|
||||
onCollapseNode={toggleNodeCollapsed}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
onContextMenu={onContextMenu}
|
||||
tabInfo={tabInfo}
|
||||
baseUIFeature={baseUIFeature}
|
||||
/>
|
||||
{openViewGroupsDialog &&
|
||||
<MythicDialog
|
||||
@@ -74,7 +198,51 @@ export const CallbacksTabsFileBrowserTree = ({ treeRootData, treeAdjMatrix, fetc
|
||||
}
|
||||
/>
|
||||
}
|
||||
{openContextMenu &&
|
||||
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
|
||||
<Dropdown
|
||||
isOpen={contextMenuData.current.dropdownAnchorRef}
|
||||
onOpen={setOpenContextMenu}
|
||||
externallyOpen={openContextMenu}
|
||||
absoluteX={contextMenuData.current.x}
|
||||
absoluteY={contextMenuData.current.y}
|
||||
anchorReference={"anchorPosition"}
|
||||
style={{
|
||||
//top: contextMenuData.current.y,
|
||||
//left: contextMenuData.current.x
|
||||
}}
|
||||
menu={[
|
||||
...contextMenuData.current?.options?.map((option, index) => (
|
||||
option.type === 'item' ? (
|
||||
<DropdownMenuItem
|
||||
key={option.name}
|
||||
disabled={option.disabled}
|
||||
onClick={(event) => handleMenuItemClick(event, option.click)}
|
||||
>
|
||||
{option.icon} {option.name}
|
||||
</DropdownMenuItem>
|
||||
) : option.type === 'menu' ? (
|
||||
<DropdownNestedMenuItem
|
||||
label={option.name}
|
||||
disabled={option.disabled}
|
||||
menu={
|
||||
option.menuItems.map((menuOption, indx) => (
|
||||
<DropdownMenuItem
|
||||
key={menuOption.name}
|
||||
disabled={menuOption.disabled}
|
||||
onClick={(event) => handleMenuItemClick(event, menuOption.click)}
|
||||
>
|
||||
{menuOption.icon}{menuOption.name}
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
}
|
||||
/>
|
||||
) : null))
|
||||
]}/>
|
||||
</ClickAwayListener>
|
||||
}
|
||||
</>
|
||||
|
||||
)
|
||||
};
|
||||
};
|
||||
export const CallbacksTabsFileBrowserTree = React.memo(CallbacksTabsFileBrowserTreePreMemo);
|
||||
@@ -8,6 +8,7 @@ import {hideCallbacksMutation} from './CallbackMutations';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import {CallbacksTableLastCheckinCell, CallbacksTablePayloadTypeCell, CallbacksTableIPCell} from "./CallbacksTableRow";
|
||||
import { DataGrid } from '@mui/x-data-grid';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
|
||||
|
||||
const callbacksAndFeaturesQuery = gql`
|
||||
@@ -22,10 +23,12 @@ query callbacksAndFeatures{
|
||||
pid
|
||||
description
|
||||
last_checkin
|
||||
dead
|
||||
payload {
|
||||
payloadtype {
|
||||
name
|
||||
id
|
||||
agent_type
|
||||
}
|
||||
}
|
||||
mythictree_groups_string
|
||||
@@ -33,7 +36,7 @@ query callbacksAndFeatures{
|
||||
}`;
|
||||
|
||||
const columns = [
|
||||
{ field: 'display_id', headerName: 'ID', width: 80, type: 'number', },
|
||||
{ field: 'display_id', headerName: 'ID', width: 70, type: 'number', },
|
||||
{
|
||||
field: 'host',
|
||||
headerName: 'Host',
|
||||
@@ -48,7 +51,7 @@ const columns = [
|
||||
field: 'pid',
|
||||
headerName: 'PID',
|
||||
type: 'number',
|
||||
width: 80,
|
||||
width: 70,
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
@@ -58,14 +61,14 @@ const columns = [
|
||||
{
|
||||
field: 'ip',
|
||||
headerName: 'IP',
|
||||
flex: 1,
|
||||
width: 100,
|
||||
renderCell: (params) => <CallbacksTableIPCell rowData={params.row} cellData={params.row.ip} />,
|
||||
sortable: false,
|
||||
valueGetter: (params) => {
|
||||
valueGetter: (value, row) => {
|
||||
try{
|
||||
return JSON.parse(params.row.ip)[0];
|
||||
return JSON.parse(row.ip)[0];
|
||||
}catch(error){
|
||||
return params.row.ip;
|
||||
return row.ip;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -73,15 +76,15 @@ const columns = [
|
||||
field: "last_checkin",
|
||||
headerName: "Checkin",
|
||||
width: 100,
|
||||
valueGetter: (params) => new Date(params.row.last_checkin),
|
||||
valueGetter: (value, row) => new Date(row.last_checkin),
|
||||
renderCell: (params) =>
|
||||
<CallbacksTableLastCheckinCell rowData={params.row} />,
|
||||
},
|
||||
{
|
||||
field: "payload.payloadtype.name",
|
||||
headerName: "Agent",
|
||||
width: 40,
|
||||
valueGetter: (params) => params.row.payload.payloadtype.name,
|
||||
flex: 0.5,
|
||||
valueGetter: (value, row) => row.payload.payloadtype.name,
|
||||
renderCell: (params) => <CallbacksTablePayloadTypeCell rowData={params.row} />
|
||||
},
|
||||
{
|
||||
@@ -90,12 +93,15 @@ const columns = [
|
||||
flex: 0.5,
|
||||
}
|
||||
];
|
||||
const CustomSelectTable = ({initialData, selectedData}) => {
|
||||
const CustomSelectTable = ({initialData, selectedData, sortModel}) => {
|
||||
const [data, setData] = React.useState([]);
|
||||
const [rowSelectionModel, setRowSelectionModel] = React.useState([]);
|
||||
const [rowSelectionModel, setRowSelectionModel] = React.useState({
|
||||
type: 'include',
|
||||
ids: new Set([]),
|
||||
});
|
||||
React.useEffect( () => {
|
||||
selectedData.current = data.reduce( (prev, cur) => {
|
||||
if(rowSelectionModel.includes(cur.id)){return [...prev, cur]}
|
||||
if(rowSelectionModel.ids.has(cur.id)){return [...prev, cur]}
|
||||
return [...prev];
|
||||
}, []);
|
||||
}, [data, rowSelectionModel]);
|
||||
@@ -115,7 +121,7 @@ const CustomSelectTable = ({initialData, selectedData}) => {
|
||||
},
|
||||
},
|
||||
sorting: {
|
||||
sortModel: [{ field: 'last_checkin', sort: 'asc' }],
|
||||
sortModel: [sortModel],
|
||||
},
|
||||
}}
|
||||
autoPageSize
|
||||
@@ -170,8 +176,13 @@ export function CallbacksTabsHideMultipleDialog({onClose}) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Hide Multiple Callbacks at Once</DialogTitle>
|
||||
<DialogContentText style={{textAlign: "center"}}>
|
||||
<b>Note: </b> Last checkin times are based on when this window opened and won't refresh.
|
||||
</DialogContentText>
|
||||
<CustomSelectTable initialData={initialData}
|
||||
selectedData={selectedData} />
|
||||
selectedData={selectedData}
|
||||
sortModel={{ field: 'last_checkin', sort: 'asc' }}
|
||||
/>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant="contained" color="primary">
|
||||
Close
|
||||
@@ -183,4 +194,49 @@ export function CallbacksTabsHideMultipleDialog({onClose}) {
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
export function CallbacksTabsSelectMultipleDialog({onClose, onSubmit}) {
|
||||
|
||||
const selectedData = React.useRef([]);
|
||||
const [initialData, setInitialData] = React.useState([]);
|
||||
useQuery(callbacksAndFeaturesQuery,{
|
||||
fetchPolicy: "no-cache",
|
||||
onCompleted: (data) => {
|
||||
const callbackData = data.callback.map( c => {
|
||||
// for each callback, get a unique set of supported features
|
||||
const display = `${c.display_id} - ${c.user}@${c.host} (${c.pid}) - ${c.description}`;
|
||||
return {...c, display};
|
||||
});
|
||||
setInitialData(callbackData);
|
||||
}
|
||||
});
|
||||
const submitTasking = () => {
|
||||
if(selectedData.current.length === 0){
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
onSubmit(selectedData.current);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Select Multiple Callbacks</DialogTitle>
|
||||
<DialogContentText style={{textAlign: "center"}}>
|
||||
<b>Note: </b> Last checkin times are based on when this window opened and won't refresh.
|
||||
</DialogContentText>
|
||||
<CustomSelectTable initialData={initialData}
|
||||
selectedData={selectedData}
|
||||
sortModel={{ field: 'display_id', sort: 'desc' }}
|
||||
/>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant="contained" color="primary">
|
||||
Close
|
||||
</Button>
|
||||
<Button onClick={submitTasking} variant="contained" color="warning">
|
||||
Use Selection
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import React, {useContext} from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import {useQuery, gql } from '@apollo/client';
|
||||
import {CallbacksTableLastCheckinCell, CallbacksTablePayloadTypeCell, CallbacksTableIPCell} from "./CallbacksTableRow";
|
||||
import { DataGrid } from '@mui/x-data-grid';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
|
||||
|
||||
const callbacksAndFeaturesQuery = gql`
|
||||
query callbacksAndFeatures{
|
||||
callback(where: {active: {_eq: true}}, order_by: {id: asc}) {
|
||||
id
|
||||
display_id
|
||||
host
|
||||
ip
|
||||
user
|
||||
process_name
|
||||
pid
|
||||
description
|
||||
last_checkin
|
||||
dead
|
||||
payload {
|
||||
payloadtype {
|
||||
name
|
||||
id
|
||||
agent_type
|
||||
}
|
||||
}
|
||||
mythictree_groups_string
|
||||
}
|
||||
}`;
|
||||
|
||||
const columns = [
|
||||
{ field: 'display_id', headerName: 'ID', width: 70, type: 'number', },
|
||||
{
|
||||
field: 'host',
|
||||
headerName: 'Host',
|
||||
flex: 0.5,
|
||||
},
|
||||
{
|
||||
field: 'user',
|
||||
headerName: 'User',
|
||||
flex: 0.5,
|
||||
},
|
||||
{
|
||||
field: 'pid',
|
||||
headerName: 'PID',
|
||||
type: 'number',
|
||||
width: 70,
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
headerName: 'Description',
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
field: 'ip',
|
||||
headerName: 'IP',
|
||||
width: 100,
|
||||
renderCell: (params) => <CallbacksTableIPCell rowData={params.row} cellData={params.row.ip} />,
|
||||
sortable: false,
|
||||
valueGetter: (value, row) => {
|
||||
try{
|
||||
return JSON.parse(row.ip)[0];
|
||||
}catch(error){
|
||||
return row.ip;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
field: "last_checkin",
|
||||
headerName: "Checkin",
|
||||
width: 100,
|
||||
valueGetter: (value, row) => new Date(row.last_checkin),
|
||||
renderCell: (params) =>
|
||||
<CallbacksTableLastCheckinCell rowData={params.row} />,
|
||||
},
|
||||
{
|
||||
field: "payload.payloadtype.name",
|
||||
headerName: "Agent",
|
||||
flex: 0.5,
|
||||
valueGetter: (value, row) => row.payload.payloadtype.name,
|
||||
renderCell: (params) => <CallbacksTablePayloadTypeCell rowData={params.row} />
|
||||
},
|
||||
{
|
||||
field: "mythictree_groups_string",
|
||||
headerName: "Groups",
|
||||
flex: 0.5,
|
||||
}
|
||||
];
|
||||
const CustomSelectTable = ({initialData, selectedData, onRowClick}) => {
|
||||
const [data, setData] = React.useState([]);
|
||||
React.useEffect( () => {
|
||||
setData(initialData.map(c => {
|
||||
return {...c};
|
||||
}));
|
||||
}, [initialData]);
|
||||
return (
|
||||
<div style={{height: "calc(80vh)"}}>
|
||||
<DataGrid
|
||||
rows={data}
|
||||
columns={columns}
|
||||
initialState={{
|
||||
pagination: {
|
||||
paginationModel: {
|
||||
},
|
||||
},
|
||||
sorting: {
|
||||
sortModel: [{ field: 'display_id', sort: 'desc' }],
|
||||
},
|
||||
}}
|
||||
autoPageSize
|
||||
onRowClick={onRowClick}
|
||||
density={"compact"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
export function CallbacksTabsOpenMultipleDialog({onClose, tabType, onOpenTabs}) {
|
||||
const selectedData = React.useRef([]);
|
||||
const [initialData, setInitialData] = React.useState([]);
|
||||
useQuery(callbacksAndFeaturesQuery,{
|
||||
fetchPolicy: "no-cache",
|
||||
onCompleted: (data) => {
|
||||
const callbackData = data.callback.map( c => {
|
||||
// for each callback, get a unique set of supported features
|
||||
const display = `${c.display_id} - ${c.user}@${c.host} (${c.pid}) - ${c.description}`;
|
||||
return {...c, display};
|
||||
});
|
||||
setInitialData(callbackData);
|
||||
}
|
||||
});
|
||||
const onRowClick = (rowData) => {
|
||||
onOpenTabs([rowData.row]);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Select Callback to open {tabType} tab</DialogTitle>
|
||||
<DialogContentText style={{textAlign: "center"}}>
|
||||
<b>Note: </b> Last checkin times are based on when this window opened and won't refresh.
|
||||
</DialogContentText>
|
||||
<CustomSelectTable initialData={initialData}
|
||||
selectedData={selectedData}
|
||||
onRowClick={onRowClick}
|
||||
/>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant="contained" color="primary">
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import {MythicTabPanel, MythicTabLabel} from '../../MythicComponents/MythicTabPanel';
|
||||
import React, {useEffect, useRef} from 'react';
|
||||
import {gql, useQuery, useSubscription } from '@apollo/client';
|
||||
import {gql, useQuery, useSubscription, useLazyQuery } from '@apollo/client';
|
||||
import { MythicDialog } from '../../MythicComponents/MythicDialog';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import {CallbacksTabsProcessBrowserTable} from './CallbacksTabsProcessBrowserTable';
|
||||
import {MythicModifyStringDialog} from '../../MythicComponents/MythicDialog';
|
||||
import {TaskFromUIButton} from './TaskFromUIButton';
|
||||
@@ -19,15 +17,17 @@ import InputLabel from '@mui/material/InputLabel';
|
||||
import Input from '@mui/material/Input';
|
||||
import {ViewCallbackMythicTreeGroupsDialog} from "./ViewCallbackMythicTreeGroupsDialog";
|
||||
import WidgetsIcon from '@mui/icons-material/Widgets';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import { Backdrop, Typography } from '@mui/material';
|
||||
import {CircularProgress} from '@mui/material';
|
||||
import ExpandIcon from '@mui/icons-material/Expand';
|
||||
import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery";
|
||||
|
||||
const treeFragment = gql`
|
||||
fragment treeObjData on mythictree {
|
||||
comment
|
||||
deleted
|
||||
task_id
|
||||
filemeta {
|
||||
id
|
||||
}
|
||||
tags {
|
||||
tagtype {
|
||||
name
|
||||
@@ -56,11 +56,11 @@ fragment treeObjData on mythictree {
|
||||
`;
|
||||
const treeSubscription = gql`
|
||||
${treeFragment}
|
||||
subscription liveData($now: timestamp!, $operation_id: Int!) {
|
||||
subscription liveData($now: timestamp!) {
|
||||
mythictree_stream(
|
||||
batch_size: 1000,
|
||||
cursor: {initial_value: {timestamp: $now}},
|
||||
where: { operation_id: { _eq: $operation_id }, tree_type: {_eq: "process"} }
|
||||
cursor: {initial_value: {timestamp: $now}},
|
||||
where: {tree_type: {_eq: "process"}, deleted: {_eq: false} }
|
||||
) {
|
||||
...treeObjData
|
||||
}
|
||||
@@ -68,13 +68,38 @@ const treeSubscription = gql`
|
||||
`;
|
||||
const rootQuery = gql`
|
||||
${treeFragment}
|
||||
query myRootFolderQuery($operation_id: Int!) {
|
||||
mythictree(where: { operation_id: { _eq: $operation_id }, tree_type: {_eq: "process"} }, order_by: {id: asc}) {
|
||||
query myRootProcessesQuery {
|
||||
mythictree(where: {parent_path_text: { _eq: "" }, tree_type: {_eq: "process"} }, order_by: {id: asc}) {
|
||||
...treeObjData
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const uniqueSplitString = "$&%^";
|
||||
const treeHostQuery = gql`
|
||||
${treeFragment}
|
||||
query processesPerHostQuery($host: String!){
|
||||
mythictree(where: {host: {_eq: $host}, tree_type: {_eq: "process"}, deleted: {_eq: false} }, order_by: {id: asc}) {
|
||||
...treeObjData
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const loadedCommandsQuery = gql`
|
||||
query loadedCommandUIFeatures($callback_id: Int!){
|
||||
loadedcommands(where: {callback_id: {_eq: $callback_id}}){
|
||||
callback_id
|
||||
command {
|
||||
supported_ui_features
|
||||
cmd
|
||||
id
|
||||
payloadtype {
|
||||
name
|
||||
id
|
||||
}
|
||||
}
|
||||
id
|
||||
version
|
||||
}
|
||||
}
|
||||
`;
|
||||
export function CallbacksTabsProcessBrowserLabel(props){
|
||||
const [description, setDescription] = React.useState("Processes: " + props.tabInfo.displayID)
|
||||
const [openEditDescriptionDialog, setOpenEditDescriptionDialog] = React.useState(false);
|
||||
@@ -105,19 +130,21 @@ export function CallbacksTabsProcessBrowserLabel(props){
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicTabLabel label={description} onDragTab={props.onDragTab} {...props} contextMenuOptions={contextMenuOptions}/>
|
||||
<MythicTabLabel label={description} highlight={props.newDataForTab[props.tabInfo.tabID]} onDragTab={props.onDragTab} {...props} contextMenuOptions={contextMenuOptions}/>
|
||||
{openEditDescriptionDialog &&
|
||||
<MythicDialog fullWidth={true} open={openEditDescriptionDialog} onClose={() => {setOpenEditDescriptionDialog(false);}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog title={"Edit Tab's Description"} onClose={() => {setOpenEditDescriptionDialog(false);}} value={description} onSubmit={editDescriptionSubmit} />
|
||||
<MythicModifyStringDialog title={"Edit Tab's Description - Displays as one line"} onClose={() => {setOpenEditDescriptionDialog(false);}} value={description} onSubmit={editDescriptionSubmit} />
|
||||
}
|
||||
/>
|
||||
}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>{
|
||||
const [fromNow, setFromNow] = React.useState((new Date()));
|
||||
export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me, setNewDataForTab}) =>{
|
||||
const fromNow = React.useRef((new Date()));
|
||||
const [backdropOpen, setBackdropOpen] = React.useState(false);
|
||||
const [expandOrCollapseAll, setExpandOrCollapseAll] = React.useState(false);
|
||||
const treeRootDataRef = React.useRef({}); // hold all the actual data
|
||||
const [treeAdjMtx, setTreeAdjMtx] = React.useState({}); // hold the simple adjacency matrix for parent/child relationships
|
||||
const [openTaskingButton, setOpenTaskingButton] = React.useState(false);
|
||||
@@ -126,12 +153,39 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>
|
||||
const [showDeletedFiles, setShowDeletedFiles] = React.useState(false);
|
||||
const [selectedHost, setSelectedHost] = React.useState("");
|
||||
const [selectedGroup, setSelectedGroup] = React.useState("");
|
||||
const loadedCommandsRef = React.useRef({});
|
||||
const loadingCommandsRef = React.useRef(false);
|
||||
const getNewMatrix = () => {
|
||||
let newMatrix = {};
|
||||
for(const[group, groupMatrix] of Object.entries(treeRootDataRef.current)){
|
||||
if(newMatrix[group] === undefined){newMatrix[group] = {}}
|
||||
for(const[host, hostMatrix] of Object.entries(treeRootDataRef.current[group])){
|
||||
if(newMatrix[group][host] === undefined){newMatrix[group][host] = {"":{}}}
|
||||
for(const[full_path_text, node] of Object.entries(treeRootDataRef.current[group][host])){
|
||||
if(newMatrix[group][host][ node["parent_path_text"] ] === undefined){
|
||||
newMatrix[group][host][ node["parent_path_text"] ] = {}
|
||||
}
|
||||
newMatrix[group][host][ node["parent_path_text"] ][full_path_text] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return newMatrix;
|
||||
}
|
||||
useQuery(rootQuery, {
|
||||
variables: { operation_id: me?.user?.current_operation_id ||0},
|
||||
onCompleted: (data) => {
|
||||
// use an adjacency matrix but only for full_path_text -> children, not both directions
|
||||
let defaultGroup = "Default";
|
||||
for(let i = 0; i < data.mythictree.length; i++){
|
||||
let currentGroups = data.mythictree[i]?.["callback"]?.["mythictree_groups"] || ["Unknown Callbacks"];
|
||||
try{
|
||||
if(data.mythictree[i]?.['callback']?.['id'] === tabInfo.callbackID){
|
||||
if(!currentGroups.includes(defaultGroup, 0)){
|
||||
defaultGroup = currentGroups[0];
|
||||
}
|
||||
}
|
||||
}catch(error){
|
||||
console.log(error);
|
||||
}
|
||||
for(let j = 0; j < currentGroups.length; j++){
|
||||
if(treeRootDataRef.current[currentGroups[j]] === undefined){
|
||||
treeRootDataRef.current[currentGroups[j]] = {};
|
||||
@@ -140,39 +194,28 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>
|
||||
// new host discovered
|
||||
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]] = {};
|
||||
}
|
||||
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"] /*+ uniqueSplitString + data.mythictree[i]["callback_id"]*/] = {...data.mythictree[i]}
|
||||
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] = {...data.mythictree[i]}
|
||||
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]].callbacks = [data.mythictree[i].callback]
|
||||
}
|
||||
}
|
||||
// create the top level data in the adjacency matrix
|
||||
const newMatrix = data.mythictree.reduce( (prev, cur) => {
|
||||
let currentGroups = cur?.["callback"]?.["mythictree_groups"] || ["Unknown Callbacks"];
|
||||
for(let j = 0; j < currentGroups.length; j++) {
|
||||
if (prev[currentGroups[j]] === undefined) {
|
||||
prev[currentGroups[j]] = {};
|
||||
}
|
||||
if (prev[currentGroups[j]][cur["host"]] === undefined) {
|
||||
// the current host isn't tracked in the adjacency matrix, so add it
|
||||
prev[currentGroups[j]][cur["host"]] = {}
|
||||
}
|
||||
if(cur["parent_path_text"] === ""){
|
||||
if(prev[currentGroups[j]][cur['host']][""] === undefined){
|
||||
prev[currentGroups[j]][cur['host']][""] = {}
|
||||
}
|
||||
prev[currentGroups[j]][cur['host']][""][cur["full_path_text"] /*+ uniqueSplitString + cur["callback_id"]*/] = 1;
|
||||
continue
|
||||
}
|
||||
if (prev[currentGroups[j]][cur["host"]][cur["parent_path_text"] /*+ uniqueSplitString + cur["callback_id"]*/] === undefined) {
|
||||
// the current parent's path isn't tracked, so add it and ourselves as children
|
||||
prev[currentGroups[j]][cur["host"]][cur["parent_path_text"] /*+ uniqueSplitString + cur["callback_id"]*/] = {};
|
||||
}
|
||||
prev[currentGroups[j]][cur["host"]][cur["parent_path_text"] /*+ uniqueSplitString + cur["callback_id"]*/][cur["full_path_text"] /*+ uniqueSplitString + cur["callback_id"]*/] = 1;
|
||||
}
|
||||
return prev;
|
||||
}, {...treeAdjMtx});
|
||||
const newMatrix = getNewMatrix();
|
||||
setTreeAdjMtx(newMatrix);
|
||||
// first see if we can find a group that matches our host, if not, then we can do first of each
|
||||
let groups = Object.keys(newMatrix).sort();
|
||||
let groups = Object.keys(newMatrix).sort();
|
||||
if(groups.length > 0){
|
||||
if(groups.includes(defaultGroup)){
|
||||
setSelectedGroup(defaultGroup);
|
||||
const hosts = Object.keys(newMatrix[defaultGroup]).sort();
|
||||
if(hosts.length > 0){
|
||||
if(hosts.includes(tabInfo.host)){
|
||||
setSelectedHost(tabInfo.host);
|
||||
} else {
|
||||
setSelectedHost(hosts[0]);
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
for(let i = 0; i < groups.length; i++){
|
||||
const hosts = Object.keys(newMatrix[groups[i]]).sort();
|
||||
if(hosts.length > 0){
|
||||
@@ -192,8 +235,21 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>
|
||||
},
|
||||
fetchPolicy: 'no-cache',
|
||||
});
|
||||
const getLoadedCommandsQuerySuccess = (data) => {
|
||||
if(data.loadedcommands.length > 0){
|
||||
loadedCommandsRef.current[data.loadedcommands[0].callback_id] = [...data.loadedcommands];
|
||||
}
|
||||
loadingCommandsRef.current = false;
|
||||
}
|
||||
const getLoadedCommandsQueryError = (data) => {
|
||||
console.log(data);
|
||||
loadingCommandsRef.current = false;
|
||||
}
|
||||
const getLoadedCommandsQuery = useMythicLazyQuery(loadedCommandsQuery, {
|
||||
fetchPolicy: 'no-cache',
|
||||
});
|
||||
useSubscription(treeSubscription, {
|
||||
variables: {now: fromNow, operation_id: me?.user?.current_operation_id ||0},
|
||||
variables: {now: fromNow.current},
|
||||
fetchPolicy: "no-cache",
|
||||
onData: ({data}) => {
|
||||
for(let i = 0; i < data.data.mythictree_stream.length; i++){
|
||||
@@ -206,36 +262,85 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>
|
||||
// new host discovered
|
||||
treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]] = {};
|
||||
}
|
||||
treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"] /*+ uniqueSplitString + data.data.mythictree_stream[i]["callback_id"]*/] = {...data.data.mythictree_stream[i]};
|
||||
if(treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]] === undefined){
|
||||
// first time we're seeing this file data, just add it
|
||||
if(data.data.mythictree_stream[i].deleted){continue}
|
||||
treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]] = {...data.data.mythictree_stream[i]};
|
||||
treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]].callbacks = [data.data.mythictree_stream[i].callback]
|
||||
} else {
|
||||
// we need to merge data in because we already have some info
|
||||
let existingData = treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]];
|
||||
if(existingData.task_id <= data.data.mythictree_stream[i].task_id){
|
||||
if(data.data.mythictree_stream[i].deleted){
|
||||
delete treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
existingData.has_children = data.data.mythictree_stream[i].has_children || existingData.has_children;
|
||||
existingData.callbacks.push(data.data.mythictree_stream[i].callback)
|
||||
existingData.comment += data.data.mythictree_stream[i].comment;
|
||||
existingData.tags = [...existingData.tags, ...data.data.mythictree_stream[i].tags];
|
||||
if(existingData.task_id > data.data.mythictree_stream[i].task_id){
|
||||
existingData.metadata = {...data.data.mythictree_stream[i].metadata, ...existingData.metadata};
|
||||
} else {
|
||||
existingData.metadata = {...existingData.metadata, ...data.data.mythictree_stream[i].metadata};
|
||||
}
|
||||
treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]] = {...existingData};
|
||||
}
|
||||
}
|
||||
}
|
||||
const newMatrix = data.data.mythictree_stream.reduce( (prev, cur) => {
|
||||
let currentGroups = cur?.["callback"]?.["mythictree_groups"] || ["Unknown Callbacks"];
|
||||
for(let j = 0; j < currentGroups.length; j++) {
|
||||
if (prev[currentGroups[j]] === undefined) {
|
||||
prev[currentGroups[j]] = {};
|
||||
}
|
||||
if (prev[currentGroups[j]][cur["host"]] === undefined) {
|
||||
// the current host isn't tracked in the adjacency matrix, so add it
|
||||
prev[currentGroups[j]][cur["host"]] = {}
|
||||
}
|
||||
if(cur["parent_path_text"] === ""){
|
||||
if(prev[currentGroups[j]][cur['host']][""] === undefined){
|
||||
prev[currentGroups[j]][cur['host']][""] = {}
|
||||
}
|
||||
prev[currentGroups[j]][cur['host']][""][cur["full_path_text"] /*+ uniqueSplitString + cur["callback_id"]*/] = 1;
|
||||
continue
|
||||
}
|
||||
if (prev[currentGroups[j]][cur["host"]][cur["parent_path_text"] /*+ uniqueSplitString + cur["callback_id"]*/] === undefined) {
|
||||
// the current parent's path isn't tracked, so add it and ourselves as children
|
||||
prev[currentGroups[j]][cur["host"]][cur["parent_path_text"] /*+ uniqueSplitString + cur["callback_id"]*/] = {};
|
||||
}
|
||||
prev[currentGroups[j]][cur["host"]][cur["parent_path_text"] /*+ uniqueSplitString + cur["callback_id"]*/][cur["full_path_text"] /*+ uniqueSplitString + cur["callback_id"]*/] = 1;
|
||||
}
|
||||
return prev;
|
||||
}, {...treeAdjMtx});
|
||||
const newMatrix = getNewMatrix();
|
||||
setTreeAdjMtx(newMatrix);
|
||||
if(index !== value){
|
||||
setNewDataForTab((prev) => {return {...prev, [tabInfo.tabID]: true}});
|
||||
}
|
||||
}
|
||||
});
|
||||
const getHostProcessesQuerySuccess = (data) => {
|
||||
snackActions.dismiss();
|
||||
// add in all of the raw data
|
||||
for(let i = 0; i < data.mythictree.length; i++){
|
||||
let currentGroups = data.mythictree[i]?.["callback"]?.["mythictree_groups"] || ["Unknown Callbacks"];
|
||||
for(let j = 0; j < currentGroups.length; j++){
|
||||
if(treeRootDataRef.current[currentGroups[j]] === undefined){
|
||||
treeRootDataRef.current[currentGroups[j]] = {};
|
||||
}
|
||||
if( treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]] === undefined) {
|
||||
// new host discovered
|
||||
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]] = {};
|
||||
}
|
||||
if(treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] === undefined){
|
||||
// first time we're seeing this file data, just add it
|
||||
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] = {...data.mythictree[i]};
|
||||
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]].callbacks = [data.mythictree[i].callback];
|
||||
} else {
|
||||
// we need to merge data in because we already have some info
|
||||
if(data.mythictree[i].deleted){
|
||||
delete treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]];
|
||||
continue;
|
||||
}
|
||||
let existingData = treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]];
|
||||
existingData.comment += data.mythictree[i].comment;
|
||||
existingData.callbacks.push(data.mythictree[i].callback)
|
||||
existingData.tags = [...existingData.tags, ...data.mythictree[i].tags];
|
||||
if(existingData.task_id > data.mythictree[i].task_id){
|
||||
existingData.metadata = {...data.mythictree[i].metadata, ...existingData.metadata};
|
||||
} else {
|
||||
existingData.metadata = {...existingData.metadata, ...data.mythictree[i].metadata};
|
||||
}
|
||||
treeRootDataRef.current[currentGroups[j]][data.mythictree[i]["host"]][data.mythictree[i]["full_path_text"]] = {...existingData};
|
||||
}
|
||||
}
|
||||
}
|
||||
// create the top level data in the adjacency matrix
|
||||
const newMatrix = getNewMatrix();
|
||||
//console.log(treeRootDataRef.current);
|
||||
setTreeAdjMtx(newMatrix);
|
||||
//console.log("just set treeAdjMtx, about to close backdrop")
|
||||
setBackdropOpen(false);
|
||||
}
|
||||
const getHostProcessesQuery = useMythicLazyQuery(treeHostQuery, {
|
||||
fetchPolicy: 'no-cache',
|
||||
})
|
||||
const onListFilesButton = () => {
|
||||
taskingData.current = ({parameters: "",
|
||||
@@ -264,6 +369,33 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>
|
||||
setSelectedHost("");
|
||||
}
|
||||
}
|
||||
async function getLoadedCommandForUIFeature (callback_id, uifeature){
|
||||
while(loadingCommandsRef.current){
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
}
|
||||
if(loadedCommandsRef.current[callback_id] === undefined){
|
||||
loadingCommandsRef.current = true;
|
||||
getLoadedCommandsQuery({variables: {callback_id: callback_id}})
|
||||
.then(({data}) => getLoadedCommandsQuerySuccess(data)).catch(({data}) => getLoadedCommandsQueryError(data));
|
||||
while(loadingCommandsRef.current){
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
}
|
||||
}
|
||||
if(loadedCommandsRef.current[callback_id] === undefined){
|
||||
return undefined;
|
||||
}
|
||||
for(let i = 0; i < loadedCommandsRef.current[callback_id].length; i++){
|
||||
if(loadedCommandsRef.current[callback_id][i].command.supported_ui_features.includes(uifeature)){
|
||||
return loadedCommandsRef.current[callback_id][i];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
React.useEffect( () => {
|
||||
getHostProcessesQuery({variables: {host: selectedHost}})
|
||||
.then(({data}) => getHostProcessesQuerySuccess(data)).catch(({data}) => console.log(data));
|
||||
setBackdropOpen(true);
|
||||
}, [selectedHost]);
|
||||
React.useEffect( () => {
|
||||
return() => {
|
||||
mountedRef.current = false;
|
||||
@@ -272,8 +404,23 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>
|
||||
}, [])
|
||||
return (
|
||||
<MythicTabPanel index={index} value={value} >
|
||||
<div style={{display: "flex", flexGrow: 1, overflowY: "auto"}}>
|
||||
<div style={{display: "flex", flexGrow: 1, overflowY: "auto", position: "relative"}}>
|
||||
|
||||
<div style={{width: "100%", display: "flex", flexDirection: "column", flexGrow: 1}}>
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute",}} invisible={false}>
|
||||
<div style={{
|
||||
borderRadius: "4px",
|
||||
border: "1px solid black",
|
||||
padding: "5px",
|
||||
backgroundColor: "rgba(37,37,37,0.92)", color: "white",
|
||||
alignItems: "center",
|
||||
display: "flex", flexDirection: "column"}}>
|
||||
<CircularProgress color="inherit" />
|
||||
<Typography variant={"h5"}>
|
||||
Gathering Processes from database for {selectedHost}...
|
||||
</Typography>
|
||||
</div>
|
||||
</Backdrop>
|
||||
<ProcessBrowserTableTop
|
||||
onListFilesButton={onListFilesButton}
|
||||
tabInfo={tabInfo}
|
||||
@@ -284,6 +431,8 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>
|
||||
updateSelectedGroup={updateSelectedGroup}
|
||||
groupOptions={treeRootDataRef.current}
|
||||
hostOptions={treeRootDataRef.current[selectedGroup] || {}}
|
||||
expandOrCollapseAll={expandOrCollapseAll}
|
||||
setExpandOrCollapseAll={setExpandOrCollapseAll}
|
||||
/>
|
||||
<CallbacksTabsProcessBrowserTable
|
||||
showDeletedFiles={showDeletedFiles}
|
||||
@@ -293,7 +442,9 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>
|
||||
treeAdjMatrix={treeAdjMtx}
|
||||
host={selectedHost}
|
||||
group={selectedGroup}
|
||||
expandOrCollapseAll={expandOrCollapseAll}
|
||||
onTaskRowAction={onTaskRowAction}
|
||||
getLoadedCommandForUIFeature={getLoadedCommandForUIFeature}
|
||||
me={me}/>
|
||||
|
||||
</div>
|
||||
@@ -304,6 +455,7 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>
|
||||
openDialog={taskingData.current?.openDialog || false}
|
||||
getConfirmation={taskingData.current?.getConfirmation || false}
|
||||
selectCallback={taskingData.current?.selectCallback || false}
|
||||
tasking_location={"process_browser"}
|
||||
onTasked={() => setOpenTaskingButton(false)}/>
|
||||
}
|
||||
</div>
|
||||
@@ -318,7 +470,9 @@ const ProcessBrowserTableTop = ({
|
||||
host,
|
||||
group,
|
||||
hostOptions,
|
||||
groupOptions
|
||||
groupOptions,
|
||||
expandOrCollapseAll,
|
||||
setExpandOrCollapseAll
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [showDeletedFiles, setLocalShowDeletedFiles] = React.useState(false);
|
||||
@@ -338,9 +492,12 @@ const ProcessBrowserTableTop = ({
|
||||
const handleGroupChange = (event) => {
|
||||
updateSelectedGroup(event.target.value);
|
||||
}
|
||||
const onLocalExpandOrCollapseAllButton = () => {
|
||||
setExpandOrCollapseAll(!expandOrCollapseAll);
|
||||
}
|
||||
return (
|
||||
<Grid container spacing={0} style={{paddingTop: "10px"}}>
|
||||
<Grid item xs={12}>
|
||||
<Grid size={12}>
|
||||
<FormControl style={{width: "30%"}}>
|
||||
<InputLabel ref={inputGroupRef}>Available Groups</InputLabel>
|
||||
<Select
|
||||
@@ -352,7 +509,7 @@ const ProcessBrowserTableTop = ({
|
||||
endAdornment={
|
||||
<React.Fragment>
|
||||
<MythicStyledTooltip title="View Callbacks associated with this group">
|
||||
<IconButton style={{padding: "3px"}} onClick={() => {setOpenViewGroupDialog(true);}} size="large"><WidgetsIcon style={{color: theme.palette.info.main}}/></IconButton>
|
||||
<IconButton style={{padding: "3px", paddingRight: "25px"}} onClick={() => {setOpenViewGroupDialog(true);}} size="large"><WidgetsIcon style={{color: theme.palette.info.main}}/></IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</React.Fragment>
|
||||
}
|
||||
@@ -375,18 +532,13 @@ const ProcessBrowserTableTop = ({
|
||||
<MythicStyledTooltip title="Task Callback to List Processes">
|
||||
<IconButton style={{padding: "3px"}} onClick={onLocalListFilesButton} size="large"><RefreshIcon style={{color: theme.palette.info.main}}/></IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={showDeletedFiles ? 'Hide Deleted Processes' : 'Show Deleted Processes'}>
|
||||
<IconButton
|
||||
style={{ padding: '3px' }}
|
||||
onClick={onLocalToggleShowDeletedFiles}
|
||||
size="large">
|
||||
{showDeletedFiles ? (
|
||||
<VisibilityIcon color="success" />
|
||||
) : (
|
||||
<VisibilityOffIcon color="error" />
|
||||
)}
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={expandOrCollapseAll ? "Collapse all processes" : "Expand all processes"} >
|
||||
<IconButton style={{padding: "3px"}}
|
||||
onClick={onLocalExpandOrCollapseAllButton} size={"large"}>
|
||||
<ExpandIcon color={expandOrCollapseAll ? "info" : "success"} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<div style={{paddingRight: "20px"}} />
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
|
||||
+449
-225
@@ -2,23 +2,13 @@ import React, {useCallback, useMemo} from 'react';
|
||||
import { alpha, IconButton } from "@mui/material";
|
||||
import {useLazyQuery, gql, useMutation } from '@apollo/client';
|
||||
import { MythicDialog, MythicViewJSONAsTableDialog, MythicModifyStringDialog } from '../../MythicComponents/MythicDialog';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import Grow from '@mui/material/Grow';
|
||||
import Popper from '@mui/material/Popper';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import MenuList from '@mui/material/MenuList';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import ListIcon from '@mui/icons-material/List';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import GetAppIcon from '@mui/icons-material/GetApp';
|
||||
import { snackActions } from '../../utilities/Snackbar';
|
||||
import 'react-virtualized/styles.css';
|
||||
import MythicResizableGrid from '../../MythicComponents/MythicResizableGrid';
|
||||
import {TableFilterDialog} from './TableFilterDialog';
|
||||
import {MythicTransferListDialog} from '../../MythicComponents/MythicTransferList';
|
||||
import {TagsDisplay, ViewEditTags} from '../../MythicComponents/MythicTag';
|
||||
import TerminalIcon from '@mui/icons-material/Terminal';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
@@ -27,8 +17,15 @@ import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import {uniqueSplitString} from './CallbacksTabsProcessBrowser';
|
||||
import {Dropdown, DropdownMenuItem, DropdownNestedMenuItem} from "../../MythicComponents/MythicNestedMenus";
|
||||
import {faSkullCrossbones, faSyringe, faKey,} from '@fortawesome/free-solid-svg-icons';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
GetComputedFontSize,
|
||||
GetMythicSetting,
|
||||
useSetMythicSetting
|
||||
} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {CallbacksTableColumnsReorderDialog} from "./CallbacksTableColumnsReorderDialog";
|
||||
|
||||
const getPermissionsDataQuery = gql`
|
||||
query getPermissionsQuery($mythictree_id: Int!) {
|
||||
@@ -46,42 +43,54 @@ const updateFileComment = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
|
||||
export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, me, onRowDoubleClick, onTaskRowAction, host, group, showDeletedFiles, tabInfo}) => {
|
||||
const columnDefaults = [
|
||||
{ name: 'Info', width: 50, disableAutosize: true, disableSort: true, disableFilterMenu: true, key: "info", visible: true },
|
||||
{ name: 'PID', type: 'number', key: 'process_id', inMetadata: true, width: 100, visible: true},
|
||||
{ name: 'PPID', type: 'number', key: 'parent_process_id', inMetadata: true, width: 100, visible: true},
|
||||
{ name: 'Name', type: 'string', disableSort: false, key: 'name_text', fillWidth: true, visible: true },
|
||||
{ name: "Arch", type: 'string', key: 'architecture', inMetadata: true, width: 70, visible: true},
|
||||
{ name: "Integrity", type: "number", key: "integrity_level", inMetadata: true, width: 100, visible: false },
|
||||
{ name: 'Session', type: 'number', key: 'session_id', inMetadata: true, width: 100, visible: true},
|
||||
{ name: "User", type: 'string', key: 'user', inMetadata: true, fillWidth: true, visible: true},
|
||||
{ name: 'Tags', type: 'tags', disableSort: true, disableFilterMenu: true, width: 220, key: "tags", visible: false },
|
||||
{ name: 'Comment', type: 'string', key: 'comment', disableSort: false, width: 200, visible: false },
|
||||
{ name: "CMD", type: "string", key: 'command_line', inMetadata: true, fillWidth: true, visible: true},
|
||||
];
|
||||
const defaultVisibleColumns = ["Info","PID", "PPID", "Name", "Arch", "Session", "User", "CMD"];
|
||||
export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, me, onRowDoubleClick,
|
||||
onTaskRowAction, host, group, showDeletedFiles, tabInfo,
|
||||
expandOrCollapseAll, getLoadedCommandForUIFeature}) => {
|
||||
//const [allData, setAllData] = React.useState([]);
|
||||
//console.log("treeAdjMatrix updated in table", treeAdjMatrix)
|
||||
const [updateSetting] = useSetMythicSetting();
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [sortData, setSortData] = React.useState({"sortKey": null, "sortDirection": null, "sortType": null});
|
||||
const [openNodes, setOpenNodes] = React.useState({});
|
||||
const [openContextMenu, setOpenContextMenu] = React.useState(false);
|
||||
const [filterOptions, setFilterOptions] = React.useState({});
|
||||
const selectedColumn = React.useRef({});
|
||||
const [columnVisibility, setColumnVisibility] = React.useState({
|
||||
"visible": ["Info","PID", "PPID", "Name", "User", "Arch", "Tags", "Comment"],
|
||||
"hidden": [ "Session" ]
|
||||
"visible": defaultVisibleColumns,
|
||||
"hidden": [ "Comment", "Tags" ]
|
||||
})
|
||||
const [singleTreeData, setSingleTreeData] = React.useState({});
|
||||
const [viewSingleTreeData, setViewSingleTreeData] = React.useState(false);
|
||||
const [openAdjustColumnsDialog, setOpenAdjustColumnsDialog] = React.useState(false);
|
||||
const [openReorderDialog, setOpenReorderDialog] = React.useState(false);
|
||||
const [columnOrder, setColumnOrder] = React.useState(columnDefaults);
|
||||
const [updatedTreeAdjMatrix, setUpdatedTreeAdjMatrix] = React.useState(treeAdjMatrix);
|
||||
const openAllNodes = () => {
|
||||
const openAllNodes = (state) => {
|
||||
let onodes = {};
|
||||
for(const [group, hosts] of Object.entries(updatedTreeAdjMatrix)){
|
||||
for(const [host, matrix] of Object.entries(hosts)){
|
||||
for(const [key, children] of Object.entries(matrix)){
|
||||
onodes[key] = true;
|
||||
//if(treeRootData[host][key] !== undefined){
|
||||
//onodes[treeRootData[host][key].id] = true;
|
||||
//onodes[parseInt(key)] = true;
|
||||
//}
|
||||
onodes[key] = state;
|
||||
}
|
||||
}
|
||||
}
|
||||
setOpenNodes(onodes);
|
||||
}
|
||||
React.useEffect( () => {
|
||||
// need to update the matrix in case there are notes that don't trace back to root
|
||||
// need to update the matrix in case there are nodes that don't trace back to root
|
||||
let adjustedMatrix = {};
|
||||
// check for cycles
|
||||
let tempMatrix = {...treeAdjMatrix};
|
||||
@@ -117,9 +126,12 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
// check if key is in children anywhere, if not, add it to adjustedMatrix[host][""][key] = 1
|
||||
let found = false;
|
||||
for(const [keySearch, childrenSearch] of Object.entries(matrix)){
|
||||
for(const [i, v] of Object.entries(childrenSearch)){
|
||||
if(i === key){found=true}
|
||||
if(childrenSearch.hasOwnProperty(key)){
|
||||
found=true;
|
||||
}
|
||||
//for(const [i, v] of Object.entries(childrenSearch)){
|
||||
// if(i === key){found=true}
|
||||
//}
|
||||
}
|
||||
if(!found){
|
||||
if(adjustedMatrix[group][host][""] === undefined){adjustedMatrix[group][host][""] = {}}
|
||||
@@ -127,18 +139,55 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
}
|
||||
}
|
||||
}
|
||||
// check for loops in our adjusted matrix
|
||||
for(const [key, _] of Object.entries(adjustedMatrix[group][host])){
|
||||
// key == 540
|
||||
// does anything have 540 has a child? 760 does - 760 is visited
|
||||
// does anything have 760 as a child? 676 does
|
||||
// does anything have 676 as a child? 540 does - X loop detected
|
||||
// let badKey = checkLoop(540, adjustedMatrix[group][host], [540]);
|
||||
let removeKey = checkLoop(adjustedMatrix[group][host], [key]);
|
||||
if(adjustedMatrix[group][host][removeKey]){
|
||||
delete adjustedMatrix[group][host][removeKey][key];
|
||||
adjustedMatrix[group][host][""][key] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//console.log("adjustedMatrix", adjustedMatrix, "realMatrix", treeAdjMatrix)
|
||||
console.log("adjustedMatrix", adjustedMatrix, "realMatrix", treeAdjMatrix)
|
||||
setUpdatedTreeAdjMatrix(adjustedMatrix);
|
||||
}, [treeAdjMatrix]);
|
||||
const checkLoop = (nodes, visited) => {
|
||||
let found = false;
|
||||
let checkingKey = visited[visited.length-1]; // the latest thing we've seen
|
||||
for(const [testKey, testNodes] of Object.entries(nodes)){
|
||||
if(testNodes.hasOwnProperty(checkingKey)){
|
||||
// we found a new node that has the last node we saw as a child
|
||||
found = true;
|
||||
//console.log("found", testNodes, "has", checkingKey, "visited", visited, "testKey", testKey)
|
||||
if(visited.includes(testKey)){
|
||||
// we found a loop
|
||||
//console.log("found loop", visited, testKey)
|
||||
return true;
|
||||
}
|
||||
visited.push(testKey);
|
||||
if(checkLoop(nodes, visited)){
|
||||
return visited.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!found){
|
||||
//console.log("didn't find", checkingKey, "in any edges")
|
||||
} else {
|
||||
//console.log("found nested, but no loop with", checkingKey)
|
||||
}
|
||||
return false;
|
||||
}
|
||||
React.useEffect( () => {
|
||||
openAllNodes();
|
||||
}, [host, group])
|
||||
React.useEffect(() => {
|
||||
openAllNodes(true);
|
||||
setViewSingleTreeData(false);
|
||||
}, [host]);
|
||||
}, [host, group])
|
||||
const onExpandNode = (nodeId) => {
|
||||
setOpenNodes({
|
||||
...openNodes,
|
||||
@@ -162,20 +211,10 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
// }
|
||||
|
||||
};
|
||||
const columnDefaults = [
|
||||
{ name: 'Info', width: 50, disableAutosize: true, disableSort: true, disableFilterMenu: true },
|
||||
{ name: 'PID', type: 'number', key: 'process_id', inMetadata: true, width: 100},
|
||||
{ name: 'PPID', type: 'number', key: 'parent_process_id', inMetadata: true, width: 100},
|
||||
{ name: 'Name', type: 'string', disableSort: false, key: 'name_text', fillWidth: true },
|
||||
{ name: "User", type: 'string', key: 'user', inMetadata: true, fillWidth: true},
|
||||
{ name: "Arch", type: 'string', key: 'architecture', inMetadata: true, width: 100},
|
||||
{ name: 'Tags', type: 'tags', disableSort: true, disableFilterMenu: true, width: 220 },
|
||||
{ name: 'Comment', type: 'string', key: 'comment', disableSort: false, width: 200 },
|
||||
{ name: 'Session', type: 'number', key: 'session_id', inMetadata: true, width: 100}
|
||||
];
|
||||
|
||||
const columns = React.useMemo(
|
||||
() =>
|
||||
columnDefaults.reduce( (prev, cur) => {
|
||||
() =>
|
||||
columnOrder.reduce( (prev, cur) => {
|
||||
if(columnVisibility.visible.includes(cur.name)){
|
||||
if(filterOptions[cur.key] && String(filterOptions[cur.key]).length > 0){
|
||||
return [...prev, {...cur, filtered: true}];
|
||||
@@ -186,7 +225,7 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
return [...prev];
|
||||
}
|
||||
}, [])
|
||||
, [filterOptions, columnVisibility]
|
||||
, [filterOptions, columnVisibility, columnOrder]
|
||||
);
|
||||
const flattenNode = useCallback(
|
||||
(node, host, group, depth = 0) => {
|
||||
@@ -353,15 +392,24 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
}
|
||||
return tempData;
|
||||
}, [allData, sortData]);
|
||||
const onSubmitFilterOptions = (newFilterOptions) => {
|
||||
setFilterOptions(newFilterOptions);
|
||||
openAllNodes();
|
||||
const onSubmitFilterOptions = (value) => {
|
||||
let newValue = value ? value : "";
|
||||
try{
|
||||
setFilterOptions({...filterOptions, [selectedColumn.current.key]: newValue });
|
||||
updateSetting({setting_name: `process_browser_filter_options`, value: {...filterOptions,[selectedColumn.current.key]: newValue }});
|
||||
}catch(error){
|
||||
console.log("failed to save filter options", error, filterOptions, selectedColumn.current.key, newValue);
|
||||
}
|
||||
if(viewSingleTreeData){
|
||||
return
|
||||
}
|
||||
openAllNodes(true);
|
||||
}
|
||||
const filterRow = (rowData) => {
|
||||
if(rowData.root){return true}
|
||||
if(!showDeletedFiles &&
|
||||
treeRootData[group][host][rowData.full_path_text /*+ uniqueSplitString + rowData.callback_id*/] !== undefined &&
|
||||
treeRootData[group][host][rowData.full_path_text /*+ uniqueSplitString + rowData.callback_id*/].deleted){
|
||||
treeRootData[group][host][rowData.full_path_text] !== undefined &&
|
||||
treeRootData[group][host][rowData.full_path_text ].deleted){
|
||||
return true;
|
||||
}
|
||||
let filterOptionInMetadata = {}
|
||||
@@ -373,13 +421,13 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
}
|
||||
}
|
||||
for(const [key,value] of Object.entries(filterOptions)){
|
||||
if(treeRootData[group][host][rowData.full_path_text /*+ uniqueSplitString + rowData.callback_id*/] === undefined){return true}
|
||||
if(treeRootData[group][host][rowData.full_path_text] === undefined){return true}
|
||||
if(filterOptionInMetadata[key]){
|
||||
if(!String(treeRootData[group][host][rowData.full_path_text /*+ uniqueSplitString + rowData.callback_id*/]?.metadata[key]).toLowerCase().includes(value)){
|
||||
if(!String(treeRootData[group][host][rowData.full_path_text ]?.metadata[key]).toLowerCase().includes(value)){
|
||||
return true;
|
||||
}
|
||||
}else{
|
||||
if(!String(treeRootData[group][host][rowData.full_path_text /*+ uniqueSplitString + rowData.callback_id*/][key]).toLowerCase().includes(value)){
|
||||
if(!String(treeRootData[group][host][rowData.full_path_text][key]).toLowerCase().includes(value)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -448,7 +496,7 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
}
|
||||
|
||||
setSingleTreeData(singleTreeAdjMatrix);
|
||||
onSubmitFilterOptions({});
|
||||
onSubmitFilterOptions(undefined);
|
||||
}
|
||||
const toggleViewSingleTreeData = () => {
|
||||
setViewSingleTreeData(!viewSingleTreeData);
|
||||
@@ -471,13 +519,14 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
viewSingleTreeData={viewSingleTreeData}
|
||||
setSingleTree={setSingleTree}
|
||||
toggleViewSingleTreeData={toggleViewSingleTreeData}
|
||||
getLoadedCommandForUIFeature={getLoadedCommandForUIFeature}
|
||||
onTaskRowAction={onTaskRowAction} />;
|
||||
case "Name":
|
||||
return <FileBrowserTableRowNameCell
|
||||
treeRootData={treeRootData[group]}
|
||||
host={host}
|
||||
group={group}
|
||||
children={updatedTreeAdjMatrix[group][host]?.[row.full_path_text /*+ uniqueSplitString + row.callback_id*/]}
|
||||
children={updatedTreeAdjMatrix[group][host]?.[row.full_path_text ]}
|
||||
handleOnClickButton={handleOnClickButton}
|
||||
rowData={row} />;
|
||||
case "User":
|
||||
@@ -485,21 +534,21 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
treeRootData={treeRootData[group]}
|
||||
host={host}
|
||||
group={group}
|
||||
cellData={treeRootData[group][host][row.full_path_text /*+ uniqueSplitString + row.callback_id*/]?.metadata?.user || ''}
|
||||
cellData={treeRootData[group][host][row.full_path_text]?.metadata?.user || ''}
|
||||
rowData={row} />;
|
||||
case "Arch":
|
||||
return <FileBrowserTableRowStringCell
|
||||
treeRootData={treeRootData[group]}
|
||||
host={host}
|
||||
group={group}
|
||||
cellData={treeRootData[group][host][row.full_path_text /*+ uniqueSplitString + row.callback_id*/]?.metadata?.architecture || ''}
|
||||
cellData={treeRootData[group][host][row.full_path_text]?.metadata?.architecture || ''}
|
||||
rowData={row} />;
|
||||
case "Session":
|
||||
return <FileBrowserTableRowStringCell
|
||||
treeRootData={treeRootData[group]}
|
||||
host={host}
|
||||
group={group}
|
||||
cellData={treeRootData[group][host][row.full_path_text /*+ uniqueSplitString + row.callback_id*/]?.metadata?.session_id || ''}
|
||||
cellData={treeRootData[group][host][row.full_path_text ]?.metadata?.session_id || ''}
|
||||
rowData={row} />;
|
||||
case "PID":
|
||||
return <FileBrowserTableRowStringCell
|
||||
@@ -507,14 +556,21 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
host={host}
|
||||
group={group}
|
||||
rowData={row}
|
||||
cellData={treeRootData[group][host][row.full_path_text /*+ uniqueSplitString + row.callback_id*/]?.metadata?.process_id || row.full_path_text} />;
|
||||
cellData={row.full_path_text} />;
|
||||
case "PPID":
|
||||
return <FileBrowserTableRowStringCell
|
||||
treeRootData={treeRootData[group]}
|
||||
host={host}
|
||||
group={group}
|
||||
rowData={row}
|
||||
cellData={treeRootData[group][host][row.full_path_text /*+ uniqueSplitString + row.callback_id*/]?.metadata?.parent_process_id || ""} />;
|
||||
cellData={treeRootData[group][host][row.full_path_text]?.parent_path_text || ""} />;
|
||||
case "Integrity":
|
||||
return <FileBrowserTableRowStringCell
|
||||
treeRootData={treeRootData[group]}
|
||||
host={host}
|
||||
group={group}
|
||||
rowData={row}
|
||||
cellData={treeRootData[group][host][row.full_path_text ]?.metadata?.integrity_level || ''} />;
|
||||
case "Tags":
|
||||
return <FileBrowserTagsCell
|
||||
rowData={row}
|
||||
@@ -528,15 +584,23 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
host={host}
|
||||
rowData={row}
|
||||
group={group}
|
||||
cellData={treeRootData[group][host][row.full_path_text /*+ uniqueSplitString + row.callback_id*/]?.comment}
|
||||
cellData={treeRootData[group][host][row.full_path_text]?.comment}
|
||||
/>;
|
||||
case "CMD":
|
||||
return <FileBrowserTableRowStringCell
|
||||
treeRootData={treeRootData[group]}
|
||||
host={host}
|
||||
rowData={row}
|
||||
group={group}
|
||||
cellData={treeRootData[group][host][row.full_path_text ]?.metadata?.command_line || ""}
|
||||
/>;
|
||||
default:
|
||||
console.log("hit default case in swith on c.name)")
|
||||
console.log("hit default case in switch on c.name)", c.name)
|
||||
}
|
||||
})];
|
||||
}
|
||||
}, []),
|
||||
[sortedData, onTaskRowAction, filterOptions, columnVisibility, showDeletedFiles]
|
||||
[sortedData, onTaskRowAction, filterOptions, columnVisibility, showDeletedFiles, treeRootData]
|
||||
);
|
||||
const onClickHeader = (e, columnIndex) => {
|
||||
const column = columns[columnIndex];
|
||||
@@ -557,13 +621,17 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
}
|
||||
};
|
||||
const localOnDoubleClick = (e, rowIndex) => {
|
||||
const rowData = treeRootData[group][host][allData[rowIndex]];
|
||||
const rowData = treeRootData[group][host][sortedData[rowIndex]["full_path_text"]];
|
||||
onRowDoubleClick(rowData);
|
||||
};
|
||||
const contextMenuOptions = [
|
||||
{
|
||||
name: 'Filter Column',
|
||||
name: 'Filter Column', type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
if(columns[columnIndex].disableFilterMenu){
|
||||
snackActions.warning("Can't filter that column");
|
||||
return;
|
||||
@@ -573,62 +641,165 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Show/Hide Columns",
|
||||
name: "Reorder Columns and Adjust Visibility", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
if(columns[columnIndex].disableFilterMenu){
|
||||
snackActions.warning("Can't filter that column");
|
||||
return;
|
||||
}
|
||||
setOpenAdjustColumnsDialog(true);
|
||||
setOpenReorderDialog(true);
|
||||
}
|
||||
}
|
||||
];
|
||||
const onSubmitAdjustColumns = ({left, right}) => {
|
||||
setColumnVisibility({visible: right, hidden: left});
|
||||
React.useEffect( () => {
|
||||
try {
|
||||
const storageItem = GetMythicSetting({setting_name: `process_browser_table_columns`, default_value: defaultVisibleColumns});
|
||||
if(storageItem !== null){
|
||||
let allColumns = [...columnVisibility["visible"].map(c => c), ...columnVisibility["hidden"].map(c => c)];
|
||||
let newHidden = [];
|
||||
allColumns.forEach((v,i,a) => {
|
||||
if(!storageItem.includes(v)){
|
||||
newHidden.push(v);
|
||||
}
|
||||
});
|
||||
if(storageItem.length !== 0){
|
||||
setColumnVisibility({visible: storageItem, hidden: newHidden});
|
||||
}
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_columns", error);
|
||||
}
|
||||
try {
|
||||
const storageItemOptions = GetMythicSetting({setting_name: `process_browser_filter_options`, default_value: {}});
|
||||
if(storageItemOptions !== null){
|
||||
setFilterOptions(storageItemOptions);
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load custom browser_table_filter_options", error);
|
||||
}
|
||||
try {
|
||||
const storageColumnOrder = GetMythicSetting({setting_name: `process_browser_column_order`, default_value: columns.map(c => c.name)});
|
||||
if(storageColumnOrder !== null){
|
||||
let newOrder = [];
|
||||
for(let i = 0; i < storageColumnOrder.length; i++){
|
||||
for(let j = 0; j < columnOrder.length; j++){
|
||||
if(columnOrder[j].name === storageColumnOrder[i]){
|
||||
newOrder.push(columnOrder[j]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
setColumnOrder(newOrder);
|
||||
}
|
||||
}catch(error){
|
||||
console.log("Failed to load process_browser_table_filter_options", error);
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
const onSubmitColumnReorder = (newOrder) => {
|
||||
let newVisible = [];
|
||||
let newHidden = [];
|
||||
for(let i = 0; i < newOrder.length; i++){
|
||||
if(newOrder[i].visible){
|
||||
newVisible.push(newOrder[i].name);
|
||||
} else {
|
||||
newHidden.push(newOrder[i].name);
|
||||
}
|
||||
}
|
||||
if(newVisible.length === 0){
|
||||
snackActions.error("Can't update to show no fields");
|
||||
return;
|
||||
}
|
||||
updateSetting({setting_name: `process_browser_column_order`, value: newOrder.map(c => c.name)});
|
||||
setColumnOrder(newOrder);
|
||||
setColumnVisibility({visible: newVisible, hidden: newHidden});
|
||||
updateSetting({setting_name: `process_browser_table_columns`, value: newVisible});
|
||||
setOpenReorderDialog(false);
|
||||
}
|
||||
const onResetColumnReorder = () => {
|
||||
onSubmitColumnReorder(columnDefaults);
|
||||
}
|
||||
|
||||
const sortColumn = columns.findIndex((column) => column.key === sortData.sortKey);
|
||||
React.useEffect( () => {
|
||||
if(viewSingleTreeData){
|
||||
return;
|
||||
}
|
||||
if(expandOrCollapseAll){
|
||||
openAllNodes(true);
|
||||
} else {
|
||||
openAllNodes(false);
|
||||
}
|
||||
}, [expandOrCollapseAll, updatedTreeAdjMatrix]);
|
||||
if(loading){
|
||||
return (
|
||||
<div style={{width: '100%', height: '100%', position: "relative",}}>
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "35%",
|
||||
top: "40%"
|
||||
}}>
|
||||
{"Loading Saved Browser Customizations..."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%', overflow: "hidden" }}>
|
||||
<div style={{ width: '100%', height: '100%', overflow: "hidden", position: "relative" }}>
|
||||
<MythicResizableGrid
|
||||
columns={columns}
|
||||
sortIndicatorIndex={sortColumn}
|
||||
sortDirection={sortData.sortDirection}
|
||||
items={gridData}
|
||||
rowHeight={35}
|
||||
rowHeight={GetComputedFontSize() + 7}
|
||||
onClickHeader={onClickHeader}
|
||||
onDoubleClickRow={localOnDoubleClick}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
/>
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xs" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={<TableFilterDialog
|
||||
selectedColumn={selectedColumn.current}
|
||||
filterOptions={filterOptions}
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
onClose={()=>{setOpenContextMenu(false);}} />}
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={String(filterOptions[selectedColumn.current.key])}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
{openAdjustColumnsDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openAdjustColumnsDialog}
|
||||
onClose={()=>{setOpenAdjustColumnsDialog(false);}}
|
||||
innerDialog={
|
||||
<MythicTransferListDialog onClose={()=>{setOpenAdjustColumnsDialog(false);}}
|
||||
onSubmit={onSubmitAdjustColumns} right={columnVisibility.visible} rightTitle="Show these columns"
|
||||
leftTitle={"Hidden Columns"} left={columnVisibility.hidden} dialogTitle={"Edit which columns are shown"}/>}
|
||||
{openReorderDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openReorderDialog}
|
||||
onClose={()=>{setOpenReorderDialog(false);}}
|
||||
innerDialog={
|
||||
<CallbacksTableColumnsReorderDialog
|
||||
onClose={()=>{setOpenReorderDialog(false);}}
|
||||
visible={columnVisibility.visible}
|
||||
hidden={columnVisibility.hidden}
|
||||
onReset={onResetColumnReorder}
|
||||
onSubmit={onSubmitColumnReorder}
|
||||
initialItems={columnOrder}
|
||||
/>}
|
||||
/>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const FileBrowserTableRowNameCell = ({ rowData, treeRootData, host, children, handleOnClickButton }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<div style={{ alignItems: 'center', display: 'flex', flexGrow: 1, width: "100%", textDecoration: treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/]?.deleted ? 'line-through' : '' }}>
|
||||
<div style={{ display: "inline-flex", height: "100%", alignItems: "center", width: "100%", textDecoration: treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/]?.deleted ? 'line-through' : '' }}>
|
||||
{[...Array(rowData.depth-1)].map((o, i) => (
|
||||
i === rowData.depth-2 && children ? (
|
||||
i === 0 ? (<div key={'folder' + rowData.full_path_text + i} style={{marginLeft: 10, paddingRight: 10}}></div>) : null
|
||||
i === 0 ? (<div key={'folder' + rowData.full_path_text + i} style={{marginLeft: 10, paddingRight: 10, height: "100%"}}></div>) : null
|
||||
) : (
|
||||
<div
|
||||
key={'folder' + rowData.full_path_text + 'lines' + i}
|
||||
@@ -644,7 +815,7 @@ const FileBrowserTableRowNameCell = ({ rowData, treeRootData, host, children, ha
|
||||
))}
|
||||
{children === undefined ? (
|
||||
<>
|
||||
<div style={{display:"inline-block", width: rowData.depth === 1 ? "1.2rem" : ""}}></div>
|
||||
<div style={{display:"inline-block", height: "100%", width: rowData.depth === 1 ? "1.2rem" : ""}}></div>
|
||||
<TerminalIcon />
|
||||
</>
|
||||
) : rowData.isOpen ? (
|
||||
@@ -659,7 +830,7 @@ const FileBrowserTableRowNameCell = ({ rowData, treeRootData, host, children, ha
|
||||
) : (
|
||||
<>
|
||||
<KeyboardArrowRightIcon
|
||||
style={{ paddingTop: '5px', }}
|
||||
style={{ }}
|
||||
onClick={() => {handleOnClickButton(rowData.full_path_text /*+ uniqueSplitString + rowData["callback_id"]*/)}} />
|
||||
<TerminalIcon />
|
||||
</>
|
||||
@@ -689,19 +860,29 @@ const FileBrowserTableRowStringCell = ({cellData, treeRootData, host, rowData})
|
||||
<div>{cellData}</div>
|
||||
)
|
||||
}
|
||||
const FileBrowserTableRowActionCell = ({rowData, onTaskRowAction, treeRootData, host, viewSingleTreeData,setSingleTree, toggleViewSingleTreeData, tabInfo}) => {
|
||||
const FileBrowserTableRowActionCell = ({rowData, onTaskRowAction, treeRootData, host, viewSingleTreeData,
|
||||
setSingleTree, toggleViewSingleTreeData, tabInfo,
|
||||
getLoadedCommandForUIFeature}) => {
|
||||
const dropdownAnchorRef = React.useRef(null);
|
||||
const theme = useTheme();
|
||||
const loadingMenuDisplay = {
|
||||
name: "Loading Dynamic Menu Items...", icon: null,
|
||||
type: "item",
|
||||
disabled: true,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
const [dropdownOpen, setDropdownOpen] = React.useState(false);
|
||||
const [viewPermissionsDialogOpen, setViewPermissionsDialogOpen] = React.useState(false);
|
||||
const [permissionData, setPermissionData] = React.useState({});
|
||||
const [fileCommentDialogOpen, setFileCommentDialogOpen] = React.useState(false);
|
||||
const [getPermissions] = useLazyQuery(getPermissionsDataQuery, {
|
||||
onCompleted: (data) => {
|
||||
console.log(rowData);
|
||||
setPermissionData({...data.mythictree_by_pk.metadata,
|
||||
callback_id: rowData["callback_id"],
|
||||
callback_display_id: rowData["callback_display_id"]});
|
||||
callback_display_id: rowData["callback_display_id"],
|
||||
callbacks: rowData['callbacks']});
|
||||
setViewPermissionsDialogOpen(true);
|
||||
},
|
||||
onError: (data) => {
|
||||
@@ -714,24 +895,13 @@ const FileBrowserTableRowActionCell = ({rowData, onTaskRowAction, treeRootData,
|
||||
snackActions.success('updated comment');
|
||||
},
|
||||
});
|
||||
const [customMenuOptions, setCustomMenuOptions] = React.useState([loadingMenuDisplay]);
|
||||
const onSubmitUpdatedComment = (comment) => {
|
||||
updateComment({ variables: { mythictree_id: treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/].id, comment: comment } });
|
||||
};
|
||||
const handleDropdownToggle = (evt) => {
|
||||
evt.stopPropagation();
|
||||
setDropdownOpen((prevOpen) => !prevOpen);
|
||||
};
|
||||
const handleMenuItemClick = (whichOption, event, index, callback_id, callback_display_id) => {
|
||||
switch (whichOption){
|
||||
case "A":
|
||||
optionsA[index].click(event, callback_id, callback_display_id);
|
||||
break;
|
||||
case "B":
|
||||
optionsB[index].click(event, callback_id, callback_display_id);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const handleMenuItemClick = (event, click) => {
|
||||
click({event});
|
||||
setDropdownOpen(false);
|
||||
};
|
||||
const handleClose = (event) => {
|
||||
@@ -739,18 +909,22 @@ const FileBrowserTableRowActionCell = ({rowData, onTaskRowAction, treeRootData,
|
||||
return;
|
||||
}
|
||||
setDropdownOpen(false);
|
||||
setCustomMenuOptions([loadingMenuDisplay]);
|
||||
};
|
||||
const optionsA = [
|
||||
{
|
||||
name: 'View Detailed Data', icon: <VisibilityIcon style={{paddingRight: "5px"}}/>, click: (evt) => {
|
||||
evt.stopPropagation();
|
||||
getPermissions({variables: {mythictree_id: rowData.id}});
|
||||
}},
|
||||
name: 'View Detailed Data', icon: <VisibilityIcon style={{paddingRight: "5px"}}/>,
|
||||
type: "item", disabled: false,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
getPermissions({variables: {mythictree_id: rowData.id}});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Edit Comment',
|
||||
name: 'Edit Comment', type: "item", disabled: false,
|
||||
icon: <EditIcon style={{ paddingRight: '5px' }} />,
|
||||
click: (evt) => {
|
||||
evt.stopPropagation();
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
setFileCommentDialogOpen(true);
|
||||
},
|
||||
},
|
||||
@@ -759,54 +933,135 @@ const FileBrowserTableRowActionCell = ({rowData, onTaskRowAction, treeRootData,
|
||||
icon: viewSingleTreeData ?
|
||||
<VisibilityOffIcon style={{paddingRight: "5px", color: theme.palette.error.main}}/> :
|
||||
<AccountTreeIcon style={{paddingRight: "5px", color: theme.palette.success.main}}/>,
|
||||
click: (evt) => {
|
||||
type: "item",
|
||||
disabled: false,
|
||||
click: ({event}) => {
|
||||
setSingleTree(rowData);
|
||||
toggleViewSingleTreeData();
|
||||
}
|
||||
}
|
||||
];
|
||||
const optionsB = [
|
||||
{name: 'Task Inject', icon: <GetAppIcon style={{paddingRight: "5px", color: theme.palette.success.main}}/>, click: (evt, callback_id, display_id) => {
|
||||
evt.stopPropagation();
|
||||
onTaskRowAction({
|
||||
process_id: treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/].metadata.process_id,
|
||||
architecture: treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/].metadata.architecture,
|
||||
uifeature: "process_browser:inject", callback_id, display_id
|
||||
});
|
||||
}},
|
||||
{name: 'Task Token Listing', icon: <ListIcon style={{paddingRight: "5px", color: theme.palette.warning.main}}/>, click: (evt, callback_id, display_id) => {
|
||||
evt.stopPropagation();
|
||||
onTaskRowAction({
|
||||
process_id: treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/].metadata.process_id,
|
||||
architecture: treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/].metadata.architecture,
|
||||
uifeature: "process_browser:list_tokens", callback_id, display_id
|
||||
});
|
||||
}, os: ["windows"]},
|
||||
{name: 'Task Steal Token', icon: <DeleteIcon style={{paddingRight: "5px", color: theme.palette.error.main}}/>, click: (evt, callback_id, display_id) => {
|
||||
evt.stopPropagation();
|
||||
onTaskRowAction({
|
||||
process_id: treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/].metadata.process_id,
|
||||
architecture: treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/].metadata.architecture,
|
||||
uifeature: "process_browser:steal_token", callback_id, display_id
|
||||
});
|
||||
|
||||
}, os: ["windows"]},
|
||||
{name: 'Task Kill Process', icon: <DeleteIcon style={{paddingRight: "5px", color: theme.palette.error.main}}/>, click: (evt, callback_id, display_id) => {
|
||||
evt.stopPropagation();
|
||||
onTaskRowAction({
|
||||
process_id: treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/].metadata.process_id,
|
||||
architecture: treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/].metadata.architecture,
|
||||
uifeature: "process_browser:kill",
|
||||
confirm_dialog: true, callback_id, display_id
|
||||
});
|
||||
|
||||
}},
|
||||
];
|
||||
async function optionsB (callback_id, callback_display_id){
|
||||
const injectCommand = await getLoadedCommandForUIFeature(callback_id, "process_browser:inject");
|
||||
let injectDisplay = "Task Inject (Unsupported)";
|
||||
if(injectCommand !== undefined){
|
||||
injectDisplay = `Task Inject (${injectCommand.command.cmd})`
|
||||
}
|
||||
const tokenListCommand = await getLoadedCommandForUIFeature(callback_id, "process_browser:list_tokens");
|
||||
let tokenListDisplay = "Task Token Listing (Unsupported)";
|
||||
if(tokenListCommand !== undefined){
|
||||
tokenListDisplay = `Task Token Listing (${tokenListCommand.command.cmd})`
|
||||
}
|
||||
const stealTokenCommand = await getLoadedCommandForUIFeature(callback_id, "process_browser:steal_token");
|
||||
let stealTokenDisplay = "Task Steal Token (Unsupported)";
|
||||
if(stealTokenCommand !== undefined){
|
||||
stealTokenDisplay = `Task Steal Token (${stealTokenCommand.command.cmd})`
|
||||
}
|
||||
const killProcessCommand = await getLoadedCommandForUIFeature(callback_id, "process_browser:kill");
|
||||
let killProcessDisplay = "Task Kill Process (Unsupported)";
|
||||
if(killProcessCommand !== undefined){
|
||||
killProcessDisplay = `Task Kill Process (${killProcessCommand.command.cmd})`
|
||||
}
|
||||
return [
|
||||
{
|
||||
name: injectDisplay, icon: <FontAwesomeIcon icon={faSyringe} style={{paddingRight: "5px", color: theme.palette.warning.main}}/>,
|
||||
type: "item",
|
||||
disabled: injectCommand === undefined,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
onTaskRowAction({
|
||||
process_id: treeRootData[host][rowData["full_path_text"] ].metadata.process_id,
|
||||
architecture: treeRootData[host][rowData["full_path_text"] ].metadata.architecture,
|
||||
uifeature: "process_browser:inject",
|
||||
callback_id,
|
||||
display_id: callback_display_id
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: tokenListDisplay, icon: <ListIcon style={{paddingRight: "5px", color: theme.palette.warning.main}}/>,
|
||||
type: "item",
|
||||
disabled: tokenListCommand === undefined,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
onTaskRowAction({
|
||||
process_id: treeRootData[host][rowData["full_path_text"] ].metadata.process_id,
|
||||
architecture: treeRootData[host][rowData["full_path_text"] ].metadata.architecture,
|
||||
uifeature: "process_browser:list_tokens",
|
||||
callback_id,
|
||||
display_id: callback_display_id
|
||||
});
|
||||
},
|
||||
os: ["Windows"]
|
||||
},
|
||||
{
|
||||
name: stealTokenDisplay, icon: <FontAwesomeIcon icon={faKey} style={{paddingRight: "5px", color: theme.palette.error.main}}/>,
|
||||
type: "item",
|
||||
disabled: stealTokenCommand === undefined,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
onTaskRowAction({
|
||||
process_id: treeRootData[host][rowData["full_path_text"] ].metadata.process_id,
|
||||
architecture: treeRootData[host][rowData["full_path_text"] ].metadata.architecture,
|
||||
uifeature: "process_browser:steal_token",
|
||||
callback_id,
|
||||
display_id: callback_display_id
|
||||
});
|
||||
|
||||
},
|
||||
os: ["Windows"]},
|
||||
{
|
||||
name: killProcessDisplay, icon: <FontAwesomeIcon icon={faSkullCrossbones} style={{paddingRight: "5px", color: theme.palette.error.main}}/>,
|
||||
type: "item",
|
||||
disabled: killProcessCommand === undefined,
|
||||
click: ({event}) => {
|
||||
event.stopPropagation();
|
||||
onTaskRowAction({
|
||||
process_id: treeRootData[host][rowData["full_path_text"] ].metadata.process_id,
|
||||
architecture: treeRootData[host][rowData["full_path_text"] ].metadata.architecture,
|
||||
uifeature: "process_browser:kill",
|
||||
confirm_dialog: true,
|
||||
callback_id,
|
||||
display_id: callback_display_id
|
||||
});
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
async function getMenuOptions() {
|
||||
let options = [...optionsA];
|
||||
options.push(...(await optionsB(tabInfo["callbackID"], tabInfo["displayID"])));
|
||||
if(treeRootData[host][rowData["full_path_text"] ]?.callback?.["id"] !== tabInfo["callbackID"]){
|
||||
options.push({
|
||||
name: `Original Callback: ${treeRootData[host][rowData["full_path_text"] ]?.callback?.["id"]}`,
|
||||
icon: null, click: () => {}, type: "menu",
|
||||
menuItems: [
|
||||
...(await optionsB(treeRootData[host][rowData["full_path_text"] ]?.callback?.["id"],
|
||||
treeRootData[host][rowData["full_path_text"] ]?.callback?.display_id))
|
||||
]
|
||||
})
|
||||
}
|
||||
return options;
|
||||
}
|
||||
const handleDropdownToggle = (evt) => {
|
||||
evt.stopPropagation();
|
||||
setDropdownOpen((prevOpen) => !prevOpen);
|
||||
if(!dropdownOpen){
|
||||
getMenuOptions().then(r => {
|
||||
if(r){
|
||||
setCustomMenuOptions(r);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setCustomMenuOptions([loadingMenuDisplay]);
|
||||
}
|
||||
};
|
||||
return (
|
||||
treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/]?.id ? (
|
||||
treeRootData[host][rowData["full_path_text"] ]?.id ? (
|
||||
<React.Fragment>
|
||||
<IconButton
|
||||
size="small"
|
||||
style={{height: "100%" }}
|
||||
aria-controls={dropdownOpen ? 'split-button-menu' : undefined}
|
||||
aria-expanded={dropdownOpen ? 'true' : undefined}
|
||||
aria-haspopup="menu"
|
||||
@@ -817,74 +1072,43 @@ const FileBrowserTableRowActionCell = ({rowData, onTaskRowAction, treeRootData,
|
||||
>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
<Popper open={dropdownOpen} anchorEl={dropdownAnchorRef.current} role={undefined} transition style={{zIndex: 4}}
|
||||
>
|
||||
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
|
||||
}}
|
||||
>
|
||||
<Paper className={"dropdownMenuColored"}>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<MenuList id="split-button-menu">
|
||||
{optionsA.map((option, index) => (
|
||||
option.os === undefined || option.os.includes(treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/].os) ? (
|
||||
<MenuItem
|
||||
key={option.name}
|
||||
onClick={(event) => handleMenuItemClick("A", event, index)}
|
||||
>
|
||||
{option.icon}{option.name}
|
||||
</MenuItem>
|
||||
) : null
|
||||
))}
|
||||
<Divider />
|
||||
<ListSubheader component={"li"} className={"MuiListSubheader-root"}>
|
||||
Act from current Callback: {tabInfo["displayID"]}
|
||||
</ListSubheader>
|
||||
{optionsB.map((option, index) => (
|
||||
option.os === undefined || option.os.includes(treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/].os) ? (
|
||||
<MenuItem
|
||||
key={option.name}
|
||||
onClick={(event) => handleMenuItemClick("B", event,
|
||||
index,
|
||||
tabInfo["callbackID"],
|
||||
tabInfo["displayID"])}
|
||||
>
|
||||
{option.icon}{option.name}
|
||||
</MenuItem>
|
||||
) : null
|
||||
))}
|
||||
{
|
||||
treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/]?.callback && treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/]?.callback?.["id"] !== tabInfo["callbackID"] &&
|
||||
<ListSubheader component={"li"} className={"MuiListSubheader-root"}>
|
||||
Act from originating Callback: {treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/]?.callback?.["display_id"] || tabInfo["displayID"]}
|
||||
</ListSubheader>
|
||||
}{
|
||||
treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/]?.callback && treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/]?.callback?.["id"] !== tabInfo["callbackID"] &&
|
||||
optionsB.map((option, index) => (
|
||||
option.os === undefined || option.os.includes(treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/].os) ? (
|
||||
<MenuItem
|
||||
{dropdownOpen &&
|
||||
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
|
||||
<Dropdown
|
||||
isOpen={dropdownAnchorRef.current}
|
||||
onOpen={setDropdownOpen}
|
||||
externallyOpen={dropdownOpen}
|
||||
anchorReference={"anchorEl"}
|
||||
menu={[
|
||||
...customMenuOptions.map((option, index) => (
|
||||
option.type === 'item' ? (
|
||||
<DropdownMenuItem
|
||||
key={option.name}
|
||||
onClick={(event) => handleMenuItemClick('B', event,
|
||||
index,
|
||||
treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/]?.["callback"]?.["id"] || tabInfo["callbackID"],
|
||||
treeRootData[host][rowData["full_path_text"] /*+ uniqueSplitString + rowData["callback_id"]*/]?.["callback"]?.["display_id"] || tabInfo["displayID"])}>
|
||||
{option.icon}
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
): null
|
||||
))
|
||||
}
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
|
||||
</Popper>
|
||||
disabled={option.disabled}
|
||||
onClick={(event) => handleMenuItemClick(event, option.click)}
|
||||
>
|
||||
{option.icon} {option.name}
|
||||
</DropdownMenuItem>
|
||||
) : option.type === 'menu' ? (
|
||||
<DropdownNestedMenuItem
|
||||
label={option.name}
|
||||
disabled={option.disabled}
|
||||
menu={
|
||||
option.menuItems.map((menuOption, indx) => (
|
||||
<DropdownMenuItem
|
||||
key={menuOption.name}
|
||||
disabled={menuOption.disabled}
|
||||
onClick={(event) => handleMenuItemClick(event, menuOption.click)}
|
||||
>
|
||||
{menuOption.icon}{menuOption.name}
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
}
|
||||
/>
|
||||
) : null))
|
||||
]}/>
|
||||
</ClickAwayListener>
|
||||
}
|
||||
{fileCommentDialogOpen && (
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
@@ -906,7 +1130,7 @@ const FileBrowserTableRowActionCell = ({rowData, onTaskRowAction, treeRootData,
|
||||
/>
|
||||
)}
|
||||
{viewPermissionsDialogOpen &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={viewPermissionsDialogOpen}
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={viewPermissionsDialogOpen}
|
||||
onClose={()=>{setViewPermissionsDialogOpen(false);}}
|
||||
innerDialog={<MythicViewJSONAsTableDialog title="View Detailed Data" leftColumn="Attribute"
|
||||
rightColumn="Value" value={permissionData}
|
||||
|
||||
@@ -9,6 +9,8 @@ import {CallbacksTabsTaskingInput} from "./CallbacksTabsTaskingInput";
|
||||
import {CallbacksTableIPCell, CallbacksTableLastCheckinCell} from "./CallbacksTableRow";
|
||||
import { DataGrid } from '@mui/x-data-grid';
|
||||
import { validate as uuidValidate } from 'uuid';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
|
||||
|
||||
const callbacksAndFeaturesQuery = gql`
|
||||
@@ -24,7 +26,13 @@ query callbacksAndFeatures($payloadtype_id: Int!) {
|
||||
display_id
|
||||
last_checkin
|
||||
ip
|
||||
dead
|
||||
mythictree_groups_string
|
||||
payload {
|
||||
payloadtype {
|
||||
agent_type
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
@@ -54,14 +62,14 @@ const columns = [
|
||||
{
|
||||
field: 'ip',
|
||||
headerName: 'IP',
|
||||
flex: 1,
|
||||
width: 100,
|
||||
renderCell: (params) => <CallbacksTableIPCell rowData={params.row} cellData={params.row.ip} />,
|
||||
sortable: false,
|
||||
valueGetter: (params) => {
|
||||
valueGetter: (value, row) => {
|
||||
try{
|
||||
return JSON.parse(params.row.ip)[0];
|
||||
return JSON.parse(row.ip)[0];
|
||||
}catch(error){
|
||||
return params.row.ip;
|
||||
return row.ip;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -69,7 +77,7 @@ const columns = [
|
||||
field: "last_checkin",
|
||||
headerName: "Checkin",
|
||||
width: 100,
|
||||
valueGetter: (params) => new Date(params.row.last_checkin),
|
||||
valueGetter: (value, row) => new Date(row.last_checkin),
|
||||
renderCell: (params) =>
|
||||
<CallbacksTableLastCheckinCell rowData={params.row} />,
|
||||
},
|
||||
@@ -81,10 +89,13 @@ const columns = [
|
||||
];
|
||||
const CustomSelectTable = ({initialData, selectedData}) => {
|
||||
const [data, setData] = React.useState([]);
|
||||
const [rowSelectionModel, setRowSelectionModel] = React.useState([]);
|
||||
const [rowSelectionModel, setRowSelectionModel] = React.useState({
|
||||
type: 'include',
|
||||
ids: new Set([]),
|
||||
});
|
||||
React.useEffect( () => {
|
||||
selectedData.current = data.reduce( (prev, cur) => {
|
||||
if(rowSelectionModel.includes(cur.id)){return [...prev, cur]}
|
||||
if(rowSelectionModel.ids.has(cur.id)){return [...prev, cur]}
|
||||
return [...prev];
|
||||
}, []);
|
||||
}, [data, rowSelectionModel]);
|
||||
@@ -119,7 +130,7 @@ const CustomSelectTable = ({initialData, selectedData}) => {
|
||||
|
||||
)
|
||||
}
|
||||
export function CallbacksTabsTaskMultipleDialog({onClose, callback, me}) {
|
||||
export function CallbacksTabsTaskMultipleDialog({onClose, callback}) {
|
||||
const mountedRef = React.useRef(true);
|
||||
const [selectedToken, setSelectedToken] = React.useState({});
|
||||
const [openTaskingButton, setOpenTaskingButton] = React.useState(false);
|
||||
@@ -139,8 +150,8 @@ export function CallbacksTabsTaskMultipleDialog({onClose, callback, me}) {
|
||||
const submitTasking = () => {
|
||||
//console.log("selectedFeature", selectedFeature)
|
||||
if(selectedData.current.length === 0){
|
||||
onClose();
|
||||
console.log("selectedData.current.length === 0")
|
||||
//onClose();
|
||||
snackActions.warning("No callbacks selected");
|
||||
return;
|
||||
}
|
||||
const callbacks = selectedData.current.map( c => c.display_id)
|
||||
@@ -163,6 +174,11 @@ export function CallbacksTabsTaskMultipleDialog({onClose, callback, me}) {
|
||||
}
|
||||
const onSubmitCommandLine = (message, cmd, parsed, force_parsed_popup, cmdGroupNames, previousTaskingLocation) => {
|
||||
//console.log(message, cmd, parsed);
|
||||
if(selectedData.current.length === 0){
|
||||
//onClose();
|
||||
snackActions.warning("No callbacks selected");
|
||||
return;
|
||||
}
|
||||
let params = message.split(" ");
|
||||
delete params[0];
|
||||
params = params.join(" ").trim();
|
||||
@@ -181,7 +197,8 @@ export function CallbacksTabsTaskMultipleDialog({onClose, callback, me}) {
|
||||
openDialog: false,
|
||||
parameters: params,
|
||||
tasking_location: newTaskingLocation,
|
||||
dontShowSuccessDialog: false
|
||||
dontShowSuccessDialog: false,
|
||||
payload_type: cmd.payloadtype?.name,
|
||||
};
|
||||
submitTasking();
|
||||
return;
|
||||
@@ -226,7 +243,8 @@ export function CallbacksTabsTaskMultipleDialog({onClose, callback, me}) {
|
||||
groupName: cmdGroupNames[0],
|
||||
parameters: params,
|
||||
tasking_location: newTaskingLocation,
|
||||
dontShowSuccessDialog: false
|
||||
dontShowSuccessDialog: false,
|
||||
payload_type: cmd.payloadtype?.name,
|
||||
};
|
||||
}else{
|
||||
finalTaskedParameters.current = undefined;
|
||||
@@ -237,7 +255,8 @@ export function CallbacksTabsTaskMultipleDialog({onClose, callback, me}) {
|
||||
parsedParameters: parsed,
|
||||
parameters: params,
|
||||
tasking_location: newTaskingLocation,
|
||||
dontShowSuccessDialog: false
|
||||
dontShowSuccessDialog: false,
|
||||
payload_type: cmd.payloadtype?.name,
|
||||
};
|
||||
}
|
||||
submitTasking();
|
||||
@@ -254,7 +273,8 @@ export function CallbacksTabsTaskMultipleDialog({onClose, callback, me}) {
|
||||
parsedParameters: parsed,
|
||||
tasking_location: newTaskingLocation,
|
||||
dontShowSuccessDialog: false,
|
||||
parameter_group_name: cmdGroupNames[0]
|
||||
parameter_group_name: cmdGroupNames[0],
|
||||
payload_type: cmd.payloadtype?.name,
|
||||
};
|
||||
submitTasking();
|
||||
}
|
||||
@@ -270,33 +290,37 @@ export function CallbacksTabsTaskMultipleDialog({onClose, callback, me}) {
|
||||
}
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Task Multiple {callback.payload.payloadtype.name} Callbacks at Once</DialogTitle>
|
||||
|
||||
<CustomSelectTable initialData={initialData}
|
||||
selectedData={selectedData} />
|
||||
<Grid item xs={12} >
|
||||
<CallbacksTabsTaskingInput filterTasks={false} onSubmitFilter={()=>{}} onSubmitCommandLine={onSubmitCommandLine}
|
||||
changeSelectedToken={changeSelectedToken}
|
||||
filterOptions={{}} callback_id={callback.id} callback_os={callback.payload.os} parentMountedRef={mountedRef} />
|
||||
</Grid>
|
||||
{openTaskingButton &&
|
||||
<TaskFromUIButton cmd={taskingData.current?.cmd}
|
||||
callback_id={taskingData?.current?.callback_id || 0}
|
||||
callback_ids={taskingData?.current?.callback_ids || undefined}
|
||||
parameters={taskingData.current?.parameters || ""}
|
||||
openDialog={taskingData.current?.openDialog || false}
|
||||
tasking_location={taskingData.current?.tasking_location || "command_line"}
|
||||
dontShowSuccessDialog={taskingData.current?.dontShowSuccessDialog || false}
|
||||
selectCallback={taskingData.current?.selectCallback || false}
|
||||
onTasked={onTasked}/>
|
||||
}
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant="contained" color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</React.Fragment>
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Task Multiple {callback.payload.payloadtype.name} Callbacks at Once</DialogTitle>
|
||||
<DialogContentText style={{textAlign: "center"}}>
|
||||
<b>Note: </b> Last checkin times are based on when this window opened and won't refresh.
|
||||
</DialogContentText>
|
||||
<CustomSelectTable initialData={initialData}
|
||||
selectedData={selectedData} />
|
||||
<Grid size={12}>
|
||||
<CallbacksTabsTaskingInput filterTasks={false} onSubmitFilter={()=>{}} onSubmitCommandLine={onSubmitCommandLine}
|
||||
changeSelectedToken={changeSelectedToken}
|
||||
payloadtype_name={callback.payload.payloadtype.name}
|
||||
hide_context={true}
|
||||
filterOptions={{}} callback_id={callback.id} callback_os={callback.payload.os} parentMountedRef={mountedRef} />
|
||||
</Grid>
|
||||
{openTaskingButton &&
|
||||
<TaskFromUIButton cmd={taskingData.current?.cmd}
|
||||
callback_id={taskingData?.current?.callback_id || 0}
|
||||
callback_ids={taskingData?.current?.callback_ids || undefined}
|
||||
parameters={taskingData.current?.parameters || ""}
|
||||
openDialog={taskingData.current?.openDialog || false}
|
||||
tasking_location={taskingData.current?.tasking_location || "command_line"}
|
||||
dontShowSuccessDialog={taskingData.current?.dontShowSuccessDialog || false}
|
||||
selectCallback={taskingData.current?.selectCallback || false}
|
||||
onTasked={onTasked}/>
|
||||
}
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant="contained" color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {MythicModifyStringDialog} from '../../MythicComponents/MythicDialog';
|
||||
import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip';
|
||||
import {taskingDataFragment, createTaskingMutation} from "./CallbackMutations";
|
||||
import { validate as uuidValidate } from 'uuid';
|
||||
import {getSkewedNow} from "../../utilities/Time";
|
||||
|
||||
|
||||
export function CallbacksTabsTaskingLabel(props){
|
||||
@@ -46,21 +47,21 @@ export function CallbacksTabsTaskingLabel(props){
|
||||
},
|
||||
]);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicTabLabel label={description} onDragTab={props.onDragTab} me={props.me} {...props} contextMenuOptions={contextMenuOptions}/>
|
||||
<>
|
||||
<MythicTabLabel highlight={props.newDataForTab[props.tabInfo.tabID]} label={description} onDragTab={props.onDragTab} me={props.me} {...props} contextMenuOptions={contextMenuOptions}/>
|
||||
{openEditDescriptionDialog &&
|
||||
<MythicDialog fullWidth={true} open={openEditDescriptionDialog} onClose={() => {setOpenEditDescriptionDialog(false);}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog title={"Edit Tab's Description"} onClose={() => {setOpenEditDescriptionDialog(false);}} value={description} onSubmit={editDescriptionSubmit} />
|
||||
<MythicModifyStringDialog title={"Edit Tab's Description - Displays as one line"} onClose={() => {setOpenEditDescriptionDialog(false);}} value={description} onSubmit={editDescriptionSubmit} />
|
||||
}
|
||||
/>
|
||||
}
|
||||
</React.Fragment>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// this is to listen for the latest tasking
|
||||
const fetchLimit = 10;
|
||||
const fetchLimit = 30;
|
||||
const getTaskingQuery = gql`
|
||||
${taskingDataFragment}
|
||||
subscription getTasking($callback_id: Int!, $fromNow: timestamp!, $limit: Int){
|
||||
@@ -81,13 +82,14 @@ query getBatchTasking($callback_id: Int!, $offset: Int!, $fetchLimit: Int!){
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, parentMountedRef, me}) =>{
|
||||
const [taskLimit, setTaskLimit] = React.useState(10);
|
||||
export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, parentMountedRef, me, collapseTaskRequest, setNewDataForTab}) =>{
|
||||
const [taskLimit, setTaskLimit] = React.useState(20);
|
||||
const [scrollToBottom, setScrollToBottom] = React.useState(false);
|
||||
const [openParametersDialog, setOpenParametersDialog] = React.useState(false);
|
||||
const [commandInfo, setCommandInfo] = React.useState({});
|
||||
const [taskingData, setTaskingData] = React.useState({task: []});
|
||||
const taskingDataRef = React.useRef({task: []});
|
||||
const [fromNow, setFromNow] = React.useState((new Date()).toISOString());
|
||||
const [fromNow, setFromNow] = React.useState(getSkewedNow().toISOString());
|
||||
const [selectedToken, setSelectedToken] = React.useState({});
|
||||
const [filterOptions, setFilterOptions] = React.useState({
|
||||
"operatorsList": [],
|
||||
@@ -173,6 +175,9 @@ export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, pa
|
||||
if(!fetched){
|
||||
setFetched(true);
|
||||
}
|
||||
if(index !== value && fetched){
|
||||
setNewDataForTab((prev) => {return {...prev, [tabInfo.tabID]: true}});
|
||||
}
|
||||
//console.log("new subscription data in CallbacksTabsTasking", subscriptionData);
|
||||
const oldLength = taskingDataRef.current.task.length;
|
||||
const mergedData = data.data.task.reduce( (prev, cur) => {
|
||||
@@ -204,23 +209,11 @@ export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, pa
|
||||
}
|
||||
useSubscription(getTaskingQuery, {
|
||||
variables: {callback_id: tabInfo.callbackID, fromNow:fromNow, limit: taskLimit},
|
||||
shouldResubscribe: true,
|
||||
onError: data => {
|
||||
console.error(data)
|
||||
},
|
||||
fetchPolicy: "no-cache",
|
||||
onData: subscriptionDataCallback});
|
||||
const scrollToBottom = useCallback( () => {
|
||||
if(taskingData && messagesEndRef.current){
|
||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
}, [taskingData, messagesEndRef]);
|
||||
useLayoutEffect( () => {
|
||||
if(canScroll){
|
||||
scrollToBottom();
|
||||
setCanScroll(false);
|
||||
}
|
||||
}, [canScroll, scrollToBottom]);
|
||||
const [getInfiniteScrollTasking, {loading: loadingMore}] = useLazyQuery(getNextBatchTaskingQuery, {
|
||||
onError: data => {
|
||||
console.error(data);
|
||||
@@ -259,6 +252,7 @@ export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, pa
|
||||
setFetchedAllTasks(false);
|
||||
}
|
||||
}
|
||||
if(!scrollToBottom){setScrollToBottom(true)}
|
||||
},
|
||||
fetchPolicy: "no-cache"
|
||||
});
|
||||
@@ -270,11 +264,16 @@ export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, pa
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
useEffect( () => {
|
||||
if(scrollToBottom){
|
||||
messagesEndRef.current.scrollIntoView();
|
||||
}
|
||||
}, [scrollToBottom]);
|
||||
const loadMoreTasks = () => {
|
||||
getInfiniteScrollTasking({variables: {callback_id: tabInfo.callbackID, offset: taskingData.task.length, fetchLimit}});
|
||||
}
|
||||
const onSubmitCommandLine = (message, cmd, parsed, force_parsed_popup, cmdGroupNames, previousTaskingLocation) => {
|
||||
//console.log(message, cmd, parsed);
|
||||
console.log(message, cmd, parsed);
|
||||
let params = message.split(" ");
|
||||
delete params[0];
|
||||
params = params.join(" ").trim();
|
||||
@@ -286,7 +285,13 @@ export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, pa
|
||||
}
|
||||
if(cmd.commandparameters.length === 0){
|
||||
// if there are no parameters, just send whatever the user types along
|
||||
onCreateTask({callback_id: tabInfo.displayID, command: cmd.cmd, params: params, parameter_group_name: "Default", tasking_location: newTaskingLocation});
|
||||
onCreateTask({callback_id: tabInfo.displayID,
|
||||
command: cmd.cmd,
|
||||
params: params,
|
||||
parameter_group_name: "Default",
|
||||
tasking_location: newTaskingLocation,
|
||||
payload_type: cmd.payloadtype?.name,
|
||||
});
|
||||
}else{
|
||||
// check if there's a "file" component that needs to be displayed
|
||||
const fileParamExists = cmd.commandparameters.find(param => {
|
||||
@@ -333,20 +338,28 @@ export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, pa
|
||||
params: JSON.stringify(parsed),
|
||||
tasking_location: newTaskingLocation,
|
||||
original_params: params,
|
||||
parameter_group_name: cmdGroupNames[0]});
|
||||
parameter_group_name: cmdGroupNames[0],
|
||||
payload_type: cmd.payloadtype?.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const submitParametersDialog = (cmd, parameters, files, selectedParameterGroup) => {
|
||||
const submitParametersDialog = (cmd, parameters, files, selectedParameterGroup, payload_type) => {
|
||||
setOpenParametersDialog(false);
|
||||
onCreateTask({callback_id: tabInfo.displayID, command: cmd, params: parameters, files: files, tasking_location: "modal", parameter_group_name: selectedParameterGroup});
|
||||
onCreateTask({callback_id: tabInfo.displayID,
|
||||
command: cmd,
|
||||
params: parameters,
|
||||
files: files,
|
||||
tasking_location: "modal",
|
||||
parameter_group_name: selectedParameterGroup,
|
||||
payload_type: payload_type
|
||||
});
|
||||
}
|
||||
const onCreateTask = ({callback_id, command, params, files, tasking_location, original_params, parameter_group_name}) => {
|
||||
//console.log(selectedToken)
|
||||
const onCreateTask = ({callback_id, command, params, files, tasking_location, original_params, parameter_group_name, payload_type}) => {
|
||||
if(selectedToken.token_id !== undefined){
|
||||
createTask({variables: {callback_id, command, params, files, token_id: selectedToken.token_id, tasking_location, original_params, parameter_group_name}});
|
||||
createTask({variables: {callback_id, command, params, files, token_id: selectedToken.token_id, tasking_location, original_params, parameter_group_name, payload_type}});
|
||||
}else{
|
||||
createTask({variables: {callback_id, command, params, files, tasking_location, original_params, parameter_group_name}});
|
||||
createTask({variables: {callback_id, command, params, files, tasking_location, original_params, parameter_group_name, payload_type}});
|
||||
}
|
||||
}
|
||||
const onSubmitFilter = (newFilter) => {
|
||||
@@ -361,30 +374,45 @@ export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, pa
|
||||
setSelectedToken(token);
|
||||
}
|
||||
}
|
||||
const [collapseAllRequest, setCollapseAllRequest] = React.useState(0);
|
||||
React.useEffect( () => {
|
||||
if(collapseTaskRequest[index] !== undefined){
|
||||
setCollapseAllRequest(collapseAllRequest + 1);
|
||||
}
|
||||
}, [collapseTaskRequest]);
|
||||
return (
|
||||
<MythicTabPanel index={index} value={value} >
|
||||
{!fetchedAllTasks &&
|
||||
<MythicStyledTooltip title="Fetch Older Tasks">
|
||||
<IconButton
|
||||
onClick={loadMoreTasks}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={{marginLeft: "50%"}}
|
||||
size="large"><AutorenewIcon /></IconButton>
|
||||
</MythicStyledTooltip>}
|
||||
{!fetched && <LinearProgress color="primary" thickness={2} style={{paddingTop: "5px"}}/>}
|
||||
{loadingMore && <LinearProgress color="primary" thickness={2} style={{paddingTop: "5px"}}/>}
|
||||
<div style={{overflowY: "auto", flexGrow: 1}}>
|
||||
{
|
||||
taskingData.task.map( (task) => (
|
||||
<TaskDisplay key={"taskinteractdisplay" + task.id} me={me} task={task} command_id={task.command == null ? 0 : task.command.id}
|
||||
filterOptions={filterOptions} newlyIssuedTasks={newlyIssuedTasks.current}/>
|
||||
))
|
||||
}
|
||||
<div style={{overflowY: "auto", flexGrow: 1, width: "100%"}} id={`taskingPanel${tabInfo.callbackID}`}>
|
||||
{!fetchedAllTasks &&
|
||||
<MythicStyledTooltip tooltipStyle={{marginLeft: "50%"}} title="Fetch Older Tasks">
|
||||
<IconButton
|
||||
onClick={loadMoreTasks}
|
||||
variant="contained"
|
||||
color="success"
|
||||
size="large"><AutorenewIcon/></IconButton>
|
||||
</MythicStyledTooltip>}
|
||||
{
|
||||
taskingData.task.map((task) => (
|
||||
<TaskDisplay key={"taskinteractdisplay" + task.id} me={me} task={task}
|
||||
command_id={task.command == null ? 0 : task.command.id}
|
||||
collapseAllRequest={collapseAllRequest}
|
||||
filterOptions={filterOptions} newlyIssuedTasks={newlyIssuedTasks.current}/>
|
||||
))
|
||||
}
|
||||
<div ref={messagesEndRef}/>
|
||||
</div>
|
||||
<div ref={messagesEndRef} />
|
||||
<CallbacksTabsTaskingInput filterTasks={true} me={me} onSubmitFilter={onSubmitFilter} onSubmitCommandLine={onSubmitCommandLine} changeSelectedToken={changeSelectedToken}
|
||||
filterOptions={filterOptions} callback_id={tabInfo.callbackID} callback_os={tabInfo.os} parentMountedRef={mountedRef} />
|
||||
|
||||
<CallbacksTabsTaskingInput filterTasks={true} me={me}
|
||||
onSubmitFilter={onSubmitFilter}
|
||||
onSubmitCommandLine={onSubmitCommandLine}
|
||||
changeSelectedToken={changeSelectedToken}
|
||||
filterOptions={filterOptions}
|
||||
focus={index === value}
|
||||
callback_id={tabInfo.callbackID}
|
||||
payloadtype_name={tabInfo.payloadtype}
|
||||
callback_os={tabInfo.os} parentMountedRef={mountedRef} />
|
||||
{openParametersDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="lg" open={openParametersDialog}
|
||||
onClose={()=>{setOpenParametersDialog(false);}}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {MythicModifyStringDialog} from '../../MythicComponents/MythicDialog';
|
||||
import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip';
|
||||
import {taskingDataFragment, createTaskingMutation} from "./CallbackMutations";
|
||||
import { validate as uuidValidate } from 'uuid';
|
||||
import {getSkewedNow} from "../../utilities/Time";
|
||||
|
||||
|
||||
export function CallbacksTabsTaskingConsoleLabel(props){
|
||||
@@ -47,11 +48,11 @@ export function CallbacksTabsTaskingConsoleLabel(props){
|
||||
]);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicTabLabel label={description} onDragTab={props.onDragTab} me={props.me} {...props} contextMenuOptions={contextMenuOptions}/>
|
||||
<MythicTabLabel label={description} highlight={props.newDataForTab[props.tabInfo.tabID]} onDragTab={props.onDragTab} me={props.me} {...props} contextMenuOptions={contextMenuOptions}/>
|
||||
{openEditDescriptionDialog &&
|
||||
<MythicDialog fullWidth={true} open={openEditDescriptionDialog} onClose={() => {setOpenEditDescriptionDialog(false);}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog title={"Edit Tab's Description"} onClose={() => {setOpenEditDescriptionDialog(false);}} value={description} onSubmit={editDescriptionSubmit} />
|
||||
<MythicModifyStringDialog title={"Edit Tab's Description - Displays as one line"} onClose={() => {setOpenEditDescriptionDialog(false);}} value={description} onSubmit={editDescriptionSubmit} />
|
||||
}
|
||||
/>
|
||||
}
|
||||
@@ -60,7 +61,7 @@ export function CallbacksTabsTaskingConsoleLabel(props){
|
||||
}
|
||||
|
||||
// this is to listen for the latest tasking
|
||||
const fetchLimit = 10;
|
||||
const fetchLimit = 20;
|
||||
const getTaskingQuery = gql`
|
||||
${taskingDataFragment}
|
||||
subscription getTasking($callback_id: Int!, $fromNow: timestamp!, $limit: Int){
|
||||
@@ -81,13 +82,14 @@ query getBatchTasking($callback_id: Int!, $offset: Int!, $fetchLimit: Int!){
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onCloseTab, parentMountedRef, me}) =>{
|
||||
export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onCloseTab, parentMountedRef, me, setNewDataForTab}) =>{
|
||||
const [taskLimit, setTaskLimit] = React.useState(10);
|
||||
const [scrollToBottom, setScrollToBottom] = React.useState(false);
|
||||
const [openParametersDialog, setOpenParametersDialog] = React.useState(false);
|
||||
const [commandInfo, setCommandInfo] = React.useState({});
|
||||
const [taskingData, setTaskingData] = React.useState({task: []});
|
||||
const taskingDataRef = React.useRef({task: []});
|
||||
const [fromNow, setFromNow] = React.useState((new Date()).toISOString());
|
||||
const [fromNow, setFromNow] = React.useState(getSkewedNow().toISOString());
|
||||
const [selectedToken, setSelectedToken] = React.useState({});
|
||||
const [filterOptions, setFilterOptions] = React.useState({
|
||||
"operatorsList": [],
|
||||
@@ -173,6 +175,9 @@ export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onClose
|
||||
if(!fetched){
|
||||
setFetched(true);
|
||||
}
|
||||
if(index !== value && fetched){
|
||||
setNewDataForTab((prev) => {return {...prev, [tabInfo.tabID]: true}});
|
||||
}
|
||||
//console.log("new subscription data in CallbacksTabsTasking", subscriptionData);
|
||||
const oldLength = taskingDataRef.current.task.length;
|
||||
const mergedData = data.data.task.reduce( (prev, cur) => {
|
||||
@@ -210,17 +215,6 @@ export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onClose
|
||||
},
|
||||
fetchPolicy: "no-cache",
|
||||
onData: subscriptionDataCallback});
|
||||
const scrollToBottom = useCallback( () => {
|
||||
if(taskingData && messagesEndRef.current){
|
||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
}, [taskingData, messagesEndRef]);
|
||||
useLayoutEffect( () => {
|
||||
if(canScroll){
|
||||
scrollToBottom();
|
||||
setCanScroll(false);
|
||||
}
|
||||
}, [canScroll, scrollToBottom]);
|
||||
const [getInfiniteScrollTasking, {loading: loadingMore}] = useLazyQuery(getNextBatchTaskingQuery, {
|
||||
onError: data => {
|
||||
console.error(data);
|
||||
@@ -259,6 +253,7 @@ export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onClose
|
||||
setFetchedAllTasks(false);
|
||||
}
|
||||
}
|
||||
if(!scrollToBottom){setScrollToBottom(true)}
|
||||
},
|
||||
fetchPolicy: "no-cache"
|
||||
});
|
||||
@@ -269,7 +264,12 @@ export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onClose
|
||||
mountedRef.current = false;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
}, []);
|
||||
useEffect( () => {
|
||||
if(scrollToBottom){
|
||||
messagesEndRef.current.scrollIntoView();
|
||||
}
|
||||
}, [scrollToBottom]);
|
||||
const loadMoreTasks = () => {
|
||||
getInfiniteScrollTasking({variables: {callback_id: tabInfo.callbackID, offset: taskingData.task.length, fetchLimit}});
|
||||
}
|
||||
@@ -286,7 +286,13 @@ export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onClose
|
||||
}
|
||||
if(cmd.commandparameters.length === 0){
|
||||
// if there are no parameters, just send whatever the user types along
|
||||
onCreateTask({callback_id: tabInfo.displayID, command: cmd.cmd, params: params, parameter_group_name: "Default", tasking_location: newTaskingLocation});
|
||||
onCreateTask({callback_id: tabInfo.displayID,
|
||||
command: cmd.cmd,
|
||||
params: params,
|
||||
parameter_group_name: "Default",
|
||||
tasking_location: newTaskingLocation,
|
||||
payload_type: cmd.payloadtype?.name,
|
||||
});
|
||||
}else{
|
||||
// check if there's a "file" component that needs to be displayed
|
||||
const fileParamExists = cmd.commandparameters.find(param => {
|
||||
@@ -325,7 +331,7 @@ export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onClose
|
||||
setCommandInfo({...cmd, "parsedParameters": parsed});
|
||||
}
|
||||
setOpenParametersDialog(true);
|
||||
return;
|
||||
|
||||
}else{
|
||||
delete parsed["_"];
|
||||
onCreateTask({callback_id: tabInfo.displayID,
|
||||
@@ -333,20 +339,28 @@ export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onClose
|
||||
params: JSON.stringify(parsed),
|
||||
tasking_location: newTaskingLocation,
|
||||
original_params: params,
|
||||
parameter_group_name: cmdGroupNames[0]});
|
||||
parameter_group_name: cmdGroupNames[0],
|
||||
payload_type: cmd.payloadtype?.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const submitParametersDialog = (cmd, parameters, files, selectedParameterGroup) => {
|
||||
const submitParametersDialog = (cmd, parameters, files, selectedParameterGroup, payload_type) => {
|
||||
setOpenParametersDialog(false);
|
||||
onCreateTask({callback_id: tabInfo.displayID, command: cmd, params: parameters, files: files, tasking_location: "modal", parameter_group_name: selectedParameterGroup});
|
||||
onCreateTask({callback_id: tabInfo.displayID,
|
||||
command: cmd,
|
||||
params: parameters,
|
||||
files: files,
|
||||
tasking_location: "modal",
|
||||
parameter_group_name: selectedParameterGroup,
|
||||
payload_type: payload_type
|
||||
});
|
||||
}
|
||||
const onCreateTask = ({callback_id, command, params, files, tasking_location, original_params, parameter_group_name}) => {
|
||||
//console.log(selectedToken)
|
||||
const onCreateTask = ({callback_id, command, params, files, tasking_location, original_params, parameter_group_name, payload_type}) => {
|
||||
if(selectedToken.token_id !== undefined){
|
||||
createTask({variables: {callback_id, command, params, files, token_id: selectedToken.token_id, tasking_location, original_params, parameter_group_name}});
|
||||
createTask({variables: {callback_id, command, params, files, token_id: selectedToken.token_id, tasking_location, original_params, parameter_group_name, payload_type}});
|
||||
}else{
|
||||
createTask({variables: {callback_id, command, params, files, tasking_location, original_params, parameter_group_name}});
|
||||
createTask({variables: {callback_id, command, params, files, tasking_location, original_params, parameter_group_name, payload_type}});
|
||||
}
|
||||
}
|
||||
const onSubmitFilter = (newFilter) => {
|
||||
@@ -363,28 +377,33 @@ export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onClose
|
||||
}
|
||||
return (
|
||||
<MythicTabPanel index={index} value={value} >
|
||||
{!fetchedAllTasks &&
|
||||
<MythicStyledTooltip title="Fetch Older Tasks">
|
||||
<IconButton
|
||||
onClick={loadMoreTasks}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={{marginLeft: "50%"}}
|
||||
size="large"><AutorenewIcon /></IconButton>
|
||||
</MythicStyledTooltip>}
|
||||
|
||||
{!fetched && <LinearProgress color="primary" thickness={2} style={{paddingTop: "5px"}}/>}
|
||||
{loadingMore && <LinearProgress color="primary" thickness={2} style={{paddingTop: "5px"}}/>}
|
||||
<div style={{overflowY: "auto", flexGrow: 1}}>
|
||||
<div style={{overflowY: "auto", flexGrow: 1, width: "100%"}} id={`taskingPanelConsole${tabInfo.callbackID}`}>
|
||||
{!fetchedAllTasks &&
|
||||
<MythicStyledTooltip title="Fetch Older Tasks" style={{marginLeft: "50%"}}>
|
||||
<IconButton
|
||||
onClick={loadMoreTasks}
|
||||
variant="contained"
|
||||
color="success"
|
||||
|
||||
size="large"><AutorenewIcon /></IconButton>
|
||||
</MythicStyledTooltip>}
|
||||
{
|
||||
taskingData.task.map( (task) => (
|
||||
<TaskDisplayConsole key={"taskinteractdisplayconsole" + task.id} me={me} task={task} command_id={task.command == null ? 0 : task.command.id}
|
||||
filterOptions={filterOptions} newlyIssuedTasks={newlyIssuedTasks.current}/>
|
||||
taskingData.task.map((task) => (
|
||||
<TaskDisplayConsole key={"taskinteractdisplayconsole" + task.id} me={me} task={task}
|
||||
command_id={task.command == null ? 0 : task.command.id}
|
||||
filterOptions={filterOptions} newlyIssuedTasks={newlyIssuedTasks.current}/>
|
||||
))
|
||||
}
|
||||
<div ref={messagesEndRef}/>
|
||||
</div>
|
||||
<div ref={messagesEndRef} />
|
||||
|
||||
<CallbacksTabsTaskingInput filterTasks={true} me={me} onSubmitFilter={onSubmitFilter} onSubmitCommandLine={onSubmitCommandLine} changeSelectedToken={changeSelectedToken}
|
||||
filterOptions={filterOptions} callback_id={tabInfo.callbackID} callback_os={tabInfo.os} parentMountedRef={mountedRef} />
|
||||
filterOptions={filterOptions} callback_id={tabInfo.callbackID}
|
||||
payloadtype_name={tabInfo.payloadtype} focus={index === value}
|
||||
callback_os={tabInfo.os} parentMountedRef={mountedRef} />
|
||||
{openParametersDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="lg" open={openParametersDialog}
|
||||
onClose={()=>{setOpenParametersDialog(false);}}
|
||||
|
||||
@@ -155,7 +155,7 @@ export function CallbacksTabsTaskingFilterDialog(props) {
|
||||
}
|
||||
return (
|
||||
<Root>
|
||||
<DialogTitle id="form-dialog-title">Filter Which Tasks Are Visible</DialogTitle>
|
||||
<DialogTitle id="form-dialog-title">Filter Which Tasks Are Visible in this Callback</DialogTitle>
|
||||
<DialogContent dividers={true} style={{overflow: "hidden"}}>
|
||||
<React.Fragment>
|
||||
<FormControl className={classes.formControl}>
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -42,7 +42,7 @@ export function CallbacksTabsTaskingInputTokenSelect(props) {
|
||||
}
|
||||
}
|
||||
return (
|
||||
<FormControl style={{width: "20%"}}>
|
||||
<FormControl style={{width: props.width ? props.width : "20%"}}>
|
||||
<Select
|
||||
labelId="demo-dialog-select-label"
|
||||
id="demo-dialog-select"
|
||||
|
||||
@@ -14,9 +14,8 @@ import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip'
|
||||
import {taskingDataFragment, createTaskingMutation} from './CallbackMutations.js';
|
||||
import Split from 'react-split';
|
||||
import {TaskDisplayContainerFlat} from "./TaskDisplayContainer";
|
||||
import {DrawTaskElementsFlowWithProvider} from "./C2PathDialog";
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import { validate as uuidValidate } from 'uuid';
|
||||
import {getSkewedNow} from "../../utilities/Time";
|
||||
|
||||
|
||||
export function CallbacksTabsTaskingSplitLabel(props){
|
||||
@@ -51,11 +50,11 @@ export function CallbacksTabsTaskingSplitLabel(props){
|
||||
]);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicTabLabel label={description} onDragTab={props.onDragTab} me={props.me} {...props} contextMenuOptions={contextMenuOptions}/>
|
||||
<MythicTabLabel label={description} highlight={props.newDataForTab[props.tabInfo.tabID]} onDragTab={props.onDragTab} me={props.me} {...props} contextMenuOptions={contextMenuOptions}/>
|
||||
{openEditDescriptionDialog &&
|
||||
<MythicDialog fullWidth={true} open={openEditDescriptionDialog} onClose={() => {setOpenEditDescriptionDialog(false);}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog title={"Edit Tab's Description"} onClose={() => {setOpenEditDescriptionDialog(false);}} value={description} onSubmit={editDescriptionSubmit} />
|
||||
<MythicModifyStringDialog title={"Edit Tab's Description - Displays as one line"} onClose={() => {setOpenEditDescriptionDialog(false);}} value={description} onSubmit={editDescriptionSubmit} />
|
||||
}
|
||||
/>
|
||||
}
|
||||
@@ -63,7 +62,7 @@ export function CallbacksTabsTaskingSplitLabel(props){
|
||||
)
|
||||
}
|
||||
// this is to listen for the latest tasking
|
||||
const fetchLimit = 10;
|
||||
const fetchLimit = 30;
|
||||
const getTaskingQuery = gql`
|
||||
${taskingDataFragment}
|
||||
subscription getTasking($callback_id: Int!, $fromNow: timestamp!, $limit: Int){
|
||||
@@ -83,13 +82,14 @@ query getBatchTasking($callback_id: Int!, $offset: Int!, $fetchLimit: Int!){
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTab, me}) =>{
|
||||
const [taskLimit, setTaskLimit] = React.useState(10);
|
||||
export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTab, me, setNewDataForTab}) =>{
|
||||
const [taskLimit, setTaskLimit] = React.useState(30);
|
||||
const [scrollToBottom, setScrollToBottom] = React.useState(false);
|
||||
const [openParametersDialog, setOpenParametersDialog] = React.useState(false);
|
||||
const [commandInfo, setCommandInfo] = React.useState({});
|
||||
const [taskingData, setTaskingData] = React.useState({task: []});
|
||||
const taskingDataRef = React.useRef({task: []});
|
||||
const [fromNow, setFromNow] = React.useState((new Date()).toISOString());
|
||||
const [fromNow, setFromNow] = React.useState(getSkewedNow().toISOString());
|
||||
const [selectedToken, setSelectedToken] = React.useState({});
|
||||
const [filterOptions, setFilterOptions] = React.useState({
|
||||
"operatorsList": [],
|
||||
@@ -99,7 +99,6 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
|
||||
"everythingButList": [],
|
||||
"hideErrors": false,
|
||||
});
|
||||
const [canScroll, setCanScroll] = React.useState(true);
|
||||
const [selectedTask, setSelectedTask] = React.useState({id: 0});
|
||||
const mountedRef = React.useRef(true);
|
||||
const newlyIssuedTasks = useRef(null);
|
||||
@@ -172,23 +171,20 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
|
||||
if(!fetched){
|
||||
setFetched(true);
|
||||
}
|
||||
if(index !== value && fetched){
|
||||
setNewDataForTab((prev) => {return {...prev, [tabInfo.tabID]: true}});
|
||||
}
|
||||
//console.log("new subscription data in CallbacksTabsTasking", subscriptionData);
|
||||
const oldLength = taskingDataRef.current.task.length;
|
||||
const mergedData = data.data.task.reduce( (prev, cur) => {
|
||||
const index = prev.findIndex(element => element.id === cur.id);
|
||||
if(index > -1){
|
||||
// need to update an element
|
||||
const updated = prev.map( (element) => {
|
||||
if(element.id === cur.id){
|
||||
if(selectedTask.id === cur.id){
|
||||
return {...cur, selected: true}
|
||||
}
|
||||
return {...cur, selected: false};
|
||||
}else{
|
||||
return {...element, selected: false};
|
||||
}
|
||||
});
|
||||
return updated;
|
||||
prev[index] = {...cur}
|
||||
if(prev[index].id === selectedTask.id){
|
||||
prev[index].selected = true;
|
||||
}
|
||||
return [...prev];
|
||||
}else{
|
||||
return [...prev, cur];
|
||||
}
|
||||
@@ -198,8 +194,8 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
|
||||
setTaskingData({task: mergedData});
|
||||
}
|
||||
if(mergedData.length > oldLength){
|
||||
setCanScroll(true);
|
||||
}
|
||||
//setCanScroll(true);
|
||||
}
|
||||
if(mergedData.length > taskLimit){
|
||||
setTaskLimit(mergedData.length);
|
||||
}
|
||||
@@ -218,17 +214,6 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
|
||||
},
|
||||
fetchPolicy: "no-cache",
|
||||
onData: subscriptionDataCallback});
|
||||
const scrollToBottom = useCallback( () => {
|
||||
if(taskingData && messagesEndRef.current){
|
||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
}, [taskingData, messagesEndRef]);
|
||||
useLayoutEffect( () => {
|
||||
if(canScroll){
|
||||
scrollToBottom();
|
||||
setCanScroll(false);
|
||||
}
|
||||
}, [canScroll, scrollToBottom]);
|
||||
const [getInfiniteScrollTasking, {loading: loadingMore}] = useLazyQuery(getNextBatchTaskingQuery, {
|
||||
onError: data => {
|
||||
console.error(data);
|
||||
@@ -270,17 +255,22 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
|
||||
setFetchedAllTasks(false);
|
||||
}
|
||||
}
|
||||
if(!scrollToBottom){setScrollToBottom(true)}
|
||||
},
|
||||
fetchPolicy: "no-cache"
|
||||
});
|
||||
useEffect( () => {
|
||||
getInfiniteScrollTasking({variables: {callback_id: tabInfo.callbackID, offset: taskingData.task.length, fetchLimit}});
|
||||
setCanScroll(true);
|
||||
return() => {
|
||||
mountedRef.current = false;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
}, []);
|
||||
useEffect( () => {
|
||||
if(scrollToBottom){
|
||||
messagesEndRef.current.scrollIntoView();
|
||||
}
|
||||
}, [scrollToBottom]);
|
||||
const loadMoreTasks = () => {
|
||||
getInfiniteScrollTasking({variables: {callback_id: tabInfo.callbackID, offset: taskingData.task.length, fetchLimit}});
|
||||
}
|
||||
@@ -297,7 +287,13 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
|
||||
}
|
||||
if(cmd.commandparameters.length === 0){
|
||||
// if there are no parameters, just send whatever the user types along
|
||||
onCreateTask({callback_id: tabInfo.displayID, command: cmd.cmd, params: params, parameter_group_name: "Default", tasking_location: newTaskingLocation});
|
||||
onCreateTask({callback_id: tabInfo.displayID,
|
||||
command: cmd.cmd,
|
||||
params: params,
|
||||
parameter_group_name: "Default",
|
||||
tasking_location: newTaskingLocation,
|
||||
payload_type: cmd.payloadtype?.name,
|
||||
});
|
||||
}else{
|
||||
// check if there's a "file" component that needs to be displayed
|
||||
const fileParamExists = cmd.commandparameters.find(param => {
|
||||
@@ -314,7 +310,8 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
|
||||
}
|
||||
}
|
||||
|
||||
});//console.log("missing File for group? ", fileParamExists, cmdGroupNames);
|
||||
});
|
||||
//console.log("missing File for group? ", fileParamExists, cmdGroupNames);
|
||||
let missingRequiredPrams = false;
|
||||
if(cmdGroupNames.length === 1){
|
||||
const missingParams = cmd.commandparameters.filter(param => param.required && param.parameter_group_name === cmdGroupNames[0] && !(param.cli_name in parsed || param.name in parsed || param.display_name in parsed));
|
||||
@@ -335,28 +332,36 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
|
||||
setCommandInfo({...cmd, "parsedParameters": parsed});
|
||||
}
|
||||
setOpenParametersDialog(true);
|
||||
return;
|
||||
|
||||
}else{
|
||||
delete parsed["_"];
|
||||
onCreateTask({callback_id: tabInfo.displayID,
|
||||
command: cmd.cmd,
|
||||
params: JSON.stringify(parsed),
|
||||
tasking_location: newTaskingLocation,
|
||||
original_params: params,
|
||||
parameter_group_name: cmdGroupNames[0]});
|
||||
}
|
||||
onCreateTask({callback_id: tabInfo.displayID,
|
||||
command: cmd.cmd,
|
||||
params: JSON.stringify(parsed),
|
||||
tasking_location: newTaskingLocation,
|
||||
original_params: params,
|
||||
parameter_group_name: cmdGroupNames[0],
|
||||
payload_type: cmd.payloadtype?.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const submitParametersDialog = (cmd, parameters, files, selectedParameterGroup) => {
|
||||
const submitParametersDialog = (cmd, parameters, files, selectedParameterGroup, payload_type) => {
|
||||
setOpenParametersDialog(false);
|
||||
onCreateTask({callback_id: tabInfo.displayID, command: cmd, params: parameters, files: files, tasking_location: "modal", parameter_group_name: selectedParameterGroup});
|
||||
onCreateTask({callback_id: tabInfo.displayID,
|
||||
command: cmd,
|
||||
params: parameters,
|
||||
files: files,
|
||||
tasking_location: "modal",
|
||||
parameter_group_name: selectedParameterGroup,
|
||||
payload_type: payload_type
|
||||
});
|
||||
}
|
||||
const onCreateTask = ({callback_id, command, params, files, tasking_location, original_params, parameter_group_name}) => {
|
||||
//console.log(selectedToken)
|
||||
const onCreateTask = ({callback_id, command, params, files, tasking_location, original_params, parameter_group_name, payload_type}) => {
|
||||
if(selectedToken.token_id !== undefined){
|
||||
createTask({variables: {callback_id, command, params, files, token_id: selectedToken.token_id, tasking_location, original_params, parameter_group_name}});
|
||||
createTask({variables: {callback_id, command, params, files, token_id: selectedToken.token_id, tasking_location, original_params, parameter_group_name, payload_type}});
|
||||
}else{
|
||||
createTask({variables: {callback_id, command, params, files, tasking_location, original_params, parameter_group_name}});
|
||||
createTask({variables: {callback_id, command, params, files, tasking_location, original_params, parameter_group_name, payload_type}});
|
||||
}
|
||||
}
|
||||
const onSubmitFilter = (newFilter) => {
|
||||
@@ -372,7 +377,7 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
|
||||
}
|
||||
}
|
||||
const changeSelectedTask = (task) => {
|
||||
let updated = {...taskingData};
|
||||
let updated = {...taskingDataRef.current};
|
||||
for(let i = 0; i < updated.task.length; i++){
|
||||
if(updated.task[i].id === task.id){
|
||||
updated.task[i].selected = true;
|
||||
@@ -397,49 +402,66 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
|
||||
let index = taskingDataRef.current.task.findIndex(e => e.id === newlyIssuedTasks.current)
|
||||
if(index > -1){
|
||||
newlyIssuedTasks.current = null;
|
||||
changeSelectedTask(taskingDataRef.current.task[index]);
|
||||
if(taskingDataRef.current.task[index]?.operator?.username === (me?.user?.username || "")){
|
||||
let el = document.getElementById(`taskingPanelSplit${tabInfo.callbackID}`);
|
||||
if(el && el.scrollHeight - el.scrollTop - el.clientHeight < 100){
|
||||
changeSelectedTask(taskingDataRef.current.task[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}, [taskingData, newlyIssuedTasks]);
|
||||
return (
|
||||
<MythicTabPanel index={index} value={value} >
|
||||
<Split direction="horizontal" minSize={[0,0]} style={{width: "100%", height: "100%", overflowY: "auto", display: "flex"}} sizes={[30, 70]} >
|
||||
<div className="bg-gray-base" style={{display: "inline-flex", flexDirection: "column"}}>
|
||||
<Split direction="horizontal" minSize={[0,0]} sizes={[30, 70]}
|
||||
style={{width: "100%", height: "100%", overflow: "auto", display: "flex", justifyContent: "flex-end"}} >
|
||||
<div className="bg-gray-base" style={{display: "inline-flex", flexDirection: "column"}} >
|
||||
|
||||
<div style={{overflowY: "auto", flexGrow: 1}}>
|
||||
<div style={{overflowY: "auto", flexGrow: 1}} id={`taskingPanelSplit${tabInfo.callbackID}`}>
|
||||
|
||||
{!fetchedAllTasks &&
|
||||
<MythicStyledTooltip title="Fetch Older Tasks">
|
||||
<IconButton
|
||||
onClick={loadMoreTasks}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={{marginLeft: "50%"}}
|
||||
size="large"><AutorenewIcon /></IconButton>
|
||||
<IconButton
|
||||
onClick={loadMoreTasks}
|
||||
variant="contained"
|
||||
color="success"
|
||||
style={{marginLeft: "50%"}}
|
||||
size="large"><AutorenewIcon/></IconButton>
|
||||
</MythicStyledTooltip>}
|
||||
{!fetched && <LinearProgress color="primary" thickness={2} style={{paddingTop: "5px"}}/>}
|
||||
{loadingMore && <LinearProgress color="info" thickness={2} style={{paddingTop: "5px"}}/>}
|
||||
{
|
||||
taskingData.task.map( (task) => (
|
||||
<TaskDisplayFlat key={"taskinteractdisplaysplit" + task.id} me={me} task={task}
|
||||
command_id={task.command == null ? 0 : task.command.id}
|
||||
filterOptions={filterOptions}
|
||||
onSelectTask={(tsk) => {changeSelectedTask(tsk)}}
|
||||
showOnSelectTask={true} selectedTask={selectedTask}
|
||||
/>
|
||||
))
|
||||
taskingData.task.map((task) => (
|
||||
<TaskDisplayFlat key={"taskinteractdisplaysplit" + task.id} me={me} task={task}
|
||||
command_id={task.command == null ? 0 : task.command.id}
|
||||
filterOptions={filterOptions}
|
||||
onSelectTask={(tsk) => {
|
||||
changeSelectedTask(tsk)
|
||||
}}
|
||||
showOnSelectTask={true} selectedTask={selectedTask}
|
||||
/>
|
||||
))
|
||||
}
|
||||
<div ref={messagesEndRef}/>
|
||||
</div>
|
||||
<div ref={messagesEndRef} />
|
||||
|
||||
</div>
|
||||
<div className="bg-gray-light" style={{display: "inline-flex", height: "100%"}}>
|
||||
<div className="bg-gray-light" style={{display: "inline-flex", flexDirection: "column", height: "100%", width: "100%"}}>
|
||||
{selectedTask.id > 0 && <TaskDisplayFlat key={"taskinteractdisplaysplit" + selectedTask.id} me={me} task={selectedTask}
|
||||
command_id={selectedTask.command == null ? 0 : selectedTask.command.id}
|
||||
filterOptions={filterOptions}
|
||||
onSelectTask={(tsk) => {
|
||||
}}
|
||||
selectedTask={selectedTask}
|
||||
/>}
|
||||
<CallbacksTabsTaskingSplitTable selectedTask={selectedTask} me={me} filterOptions={filterOptions}
|
||||
onSelectTask={(tsk) => {changeSelectedTask(tsk)}} changeSelectedTask={changeSelectedTask}
|
||||
/>
|
||||
</div>
|
||||
</Split>
|
||||
<CallbacksTabsTaskingInput filterTasks={true} me={me} onSubmitFilter={onSubmitFilter}
|
||||
payloadtype_name={tabInfo.payloadtype} focus={index === value}
|
||||
onSubmitCommandLine={onSubmitCommandLine} changeSelectedToken={changeSelectedToken}
|
||||
filterOptions={filterOptions} callback_id={tabInfo.callbackID}
|
||||
callback_os={tabInfo.os} parentMountedRef={mountedRef} />
|
||||
@@ -456,44 +478,12 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
|
||||
</MythicTabPanel>
|
||||
);
|
||||
}
|
||||
const CallbacksTabsTaskingSplitTable = ({selectedTask, me, changeSelectedTask, onSelectTask}) => {
|
||||
const [edges, setEdges] = React.useState([]);
|
||||
const theme = useTheme();
|
||||
useEffect(() => {
|
||||
let initialEdge = {
|
||||
source: {...selectedTask, selected: true, onSelectTask },
|
||||
destination: {...selectedTask, selectedTask: true, onSelectTask},
|
||||
}
|
||||
const newEdges = selectedTask?.tasks?.reduce( (prev, cur) => {
|
||||
let nextEdge = {
|
||||
source: {...selectedTask, selected: false, onSelectTask},
|
||||
destination: {...cur, selected: false, onSelectTask},
|
||||
}
|
||||
return [...prev, nextEdge];
|
||||
}, [initialEdge]) || [initialEdge];
|
||||
setEdges(newEdges);
|
||||
}, [selectedTask]);
|
||||
const CallbacksTabsTaskingSplitTable = ({selectedTask, me}) => {
|
||||
return (
|
||||
<Split direction="vertical" style={{width: "100%", height: "100%", overflowY: "auto", display: "flex", flexDirection: "column"}} minSize={[0,0]} sizes={[0, 100]} >
|
||||
<div className="bg-gray-base" style={{display: "inline-flex", height: "100%", width: "100%"}}>
|
||||
{
|
||||
selectedTask.id > 0 &&
|
||||
<DrawTaskElementsFlowWithProvider theme={theme} edges={edges} view_config={{group_by: "name", rankDir: "LR",}} />
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-light" style={{display: "inline-flex", height: "100%", width: "100%", flexDirection: "column"}}>
|
||||
<div style={{width: "100%", height: "100%", overflowY: "auto", display: "flex", flexDirection: "column"}} >
|
||||
{selectedTask.id > 0 &&
|
||||
<TaskDisplayContainerFlat me={me} task={selectedTask} />
|
||||
<TaskDisplayContainerFlat key={selectedTask.id} me={me} task={selectedTask} />
|
||||
}
|
||||
</div>
|
||||
</Split>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/*
|
||||
{selectedTask.id > 0 &&
|
||||
<TaskDisplayFlat key={"taskinteractdisplaytable" + selectedTask.id} me={me} task={selectedTask}
|
||||
command_id={selectedTask.command == null ? 0 : selectedTask.command.id}
|
||||
filterOptions={filterOptions} showOnSelectTask={false} onSelectTask={onSelectTask}/>
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -1,18 +1,23 @@
|
||||
import React, {createContext} from 'react';
|
||||
import {useSubscription, gql } from '@apollo/client';
|
||||
import {CallbacksTable, CallbacksTableMaterialReactTable} from './CallbacksTable';
|
||||
import {useSubscription, gql, useQuery } from '@apollo/client';
|
||||
import {CallbacksTable} from './CallbacksTable';
|
||||
import {CallbacksGraph} from './CallbacksGraph';
|
||||
import {useMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {getSkewedNow} from "../../utilities/Time";
|
||||
export const CallbackGraphEdgesContext = createContext([]);
|
||||
export const OnOpenTabContext = createContext( () => {});
|
||||
export const OnOpenTabsContext = createContext( () => {});
|
||||
export const CallbacksContext = createContext([]);
|
||||
//
|
||||
//callback(where: {active: {_eq: true}}){
|
||||
const SUB_Callbacks = gql`
|
||||
subscription CallbacksSubscription{
|
||||
callback_stream(batch_size: 1000, cursor: {initial_value: {timestamp: "1970-01-01"}}) {
|
||||
subscription CallbacksSubscription($fromNow: timestamp!){
|
||||
callback_stream(batch_size: 1000, cursor: {initial_value: {timestamp: $fromNow}}) {
|
||||
architecture
|
||||
cwd
|
||||
impersonation_context
|
||||
active
|
||||
dead
|
||||
color
|
||||
display_id
|
||||
description
|
||||
domain
|
||||
@@ -33,10 +38,28 @@ callback_stream(batch_size: 1000, cursor: {initial_value: {timestamp: "1970-01-0
|
||||
user
|
||||
agent_callback_id
|
||||
operation_id
|
||||
process_name
|
||||
process_short_name
|
||||
last_checkin
|
||||
current_time
|
||||
mythictree_groups
|
||||
trigger_on_checkin_after_time
|
||||
tags {
|
||||
tagtype {
|
||||
name
|
||||
color
|
||||
id
|
||||
}
|
||||
id
|
||||
}
|
||||
callbackports(where: {deleted: {_eq: false}}) {
|
||||
local_port
|
||||
remote_port
|
||||
remote_ip
|
||||
port_type
|
||||
username
|
||||
password
|
||||
id
|
||||
}
|
||||
payload {
|
||||
os
|
||||
payloadtype {
|
||||
@@ -50,9 +73,69 @@ callback_stream(batch_size: 1000, cursor: {initial_value: {timestamp: "1970-01-0
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const SUB_Edges = gql`
|
||||
subscription CallbacksSubscription{
|
||||
callbackgraphedge(order_by: {id: desc, end_timestamp: desc_nulls_first}) {
|
||||
export const Query_Callbacks_And_Edges = gql`
|
||||
query InitialActiveCallbacksQuery{
|
||||
callback(where: {active: {_eq: true}}) {
|
||||
architecture
|
||||
cwd
|
||||
impersonation_context
|
||||
active
|
||||
dead
|
||||
color
|
||||
display_id
|
||||
description
|
||||
domain
|
||||
external_ip
|
||||
host
|
||||
id
|
||||
integrity_level
|
||||
ip
|
||||
locked
|
||||
locked_operator {
|
||||
username
|
||||
id
|
||||
}
|
||||
extra_info
|
||||
sleep_info
|
||||
pid
|
||||
os
|
||||
user
|
||||
agent_callback_id
|
||||
operation_id
|
||||
process_short_name
|
||||
last_checkin
|
||||
current_time
|
||||
mythictree_groups
|
||||
trigger_on_checkin_after_time
|
||||
tags {
|
||||
tagtype {
|
||||
name
|
||||
color
|
||||
id
|
||||
}
|
||||
id
|
||||
}
|
||||
callbackports(where: {deleted: {_eq: false}}) {
|
||||
id
|
||||
local_port
|
||||
remote_port
|
||||
remote_ip
|
||||
port_type
|
||||
username
|
||||
password
|
||||
}
|
||||
payload {
|
||||
os
|
||||
payloadtype {
|
||||
name
|
||||
agent_type
|
||||
id
|
||||
}
|
||||
description
|
||||
id
|
||||
}
|
||||
}
|
||||
callbackgraphedge(order_by: {id: desc, end_timestamp: desc_nulls_first}) {
|
||||
id
|
||||
end_timestamp
|
||||
destination {
|
||||
@@ -68,6 +151,14 @@ subscription CallbacksSubscription{
|
||||
process_name
|
||||
integrity_level
|
||||
extra_info
|
||||
tags {
|
||||
tagtype {
|
||||
name
|
||||
color
|
||||
id
|
||||
}
|
||||
id
|
||||
}
|
||||
payload {
|
||||
payloadtype {
|
||||
name
|
||||
@@ -77,6 +168,8 @@ subscription CallbacksSubscription{
|
||||
callbackc2profiles {
|
||||
c2profile {
|
||||
name
|
||||
has_logo
|
||||
is_p2p
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,6 +186,14 @@ subscription CallbacksSubscription{
|
||||
process_name
|
||||
integrity_level
|
||||
extra_info
|
||||
tags {
|
||||
tagtype {
|
||||
name
|
||||
color
|
||||
id
|
||||
}
|
||||
id
|
||||
}
|
||||
payload {
|
||||
payloadtype {
|
||||
name
|
||||
@@ -102,6 +203,8 @@ subscription CallbacksSubscription{
|
||||
callbackc2profiles {
|
||||
c2profile {
|
||||
name
|
||||
has_logo
|
||||
is_p2p
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,96 +212,246 @@ subscription CallbacksSubscription{
|
||||
id
|
||||
is_p2p
|
||||
name
|
||||
has_logo
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const SUB_Edges = gql`
|
||||
subscription CallbackEdgeSubscription($fromNow: timestamp!){
|
||||
callbackgraphedge_stream(batch_size: 100, cursor: {initial_value: {updated_at: $fromNow}}) {
|
||||
id
|
||||
end_timestamp
|
||||
destination {
|
||||
active
|
||||
id
|
||||
color
|
||||
display_id
|
||||
operation_id
|
||||
user
|
||||
host
|
||||
ip
|
||||
domain
|
||||
os
|
||||
process_name
|
||||
integrity_level
|
||||
extra_info
|
||||
tags {
|
||||
tagtype {
|
||||
name
|
||||
color
|
||||
id
|
||||
}
|
||||
id
|
||||
}
|
||||
payload {
|
||||
payloadtype {
|
||||
name
|
||||
id
|
||||
}
|
||||
}
|
||||
callbackc2profiles {
|
||||
c2profile {
|
||||
name
|
||||
has_logo
|
||||
is_p2p
|
||||
}
|
||||
}
|
||||
}
|
||||
source {
|
||||
active
|
||||
id
|
||||
color
|
||||
display_id
|
||||
operation_id
|
||||
user
|
||||
host
|
||||
ip
|
||||
domain
|
||||
os
|
||||
process_name
|
||||
integrity_level
|
||||
extra_info
|
||||
tags {
|
||||
tagtype {
|
||||
name
|
||||
color
|
||||
id
|
||||
}
|
||||
id
|
||||
}
|
||||
payload {
|
||||
payloadtype {
|
||||
name
|
||||
id
|
||||
}
|
||||
}
|
||||
callbackc2profiles {
|
||||
c2profile {
|
||||
name
|
||||
has_logo
|
||||
is_p2p
|
||||
}
|
||||
}
|
||||
}
|
||||
c2profile {
|
||||
id
|
||||
is_p2p
|
||||
name
|
||||
has_logo
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export function CallbacksTop(props){
|
||||
const me = props.me;
|
||||
const [callbacks, setCallbacks] = React.useState([]);
|
||||
const [callbackEdges, setCallbackEdges] = React.useState([]);
|
||||
const callbacks = React.useRef([]);
|
||||
const callbackEdges = React.useRef([]);
|
||||
const fromNow = React.useRef(getSkewedNow());
|
||||
const mountedRef = React.useRef(true);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
useSubscription(SUB_Callbacks, {
|
||||
variables: {fromNow: fromNow.current},
|
||||
fetchPolicy: "no-cache",
|
||||
onData: ({data}) => {
|
||||
if(!mountedRef.current){
|
||||
return;
|
||||
}
|
||||
if(!mountedRef.current){
|
||||
return;
|
||||
}
|
||||
const updated = data.data.callback_stream.reduce( (prev, cur) => {
|
||||
let existingIndex = prev.findIndex( (element, i, array) => element.id === cur.id);
|
||||
if(existingIndex === -1){
|
||||
// cur isn't in our current list of callbacks
|
||||
if(cur.active){return [...prev, cur]}
|
||||
}
|
||||
if(!cur.active){
|
||||
if(existingIndex !== -1){
|
||||
prev.splice(existingIndex, 1);
|
||||
return [...prev];
|
||||
}
|
||||
|
||||
const updated = data.data.callback_stream.reduce( (prev, cur) => {
|
||||
console.log(cur)
|
||||
|
||||
let existingIndex = prev.findIndex( (element, i, array) => element.id === cur.id);
|
||||
if(existingIndex === -1){
|
||||
// cur isn't in our current list of callbacks
|
||||
if(cur.active){return [...prev, cur]}
|
||||
}
|
||||
if(!cur.active){
|
||||
if(existingIndex !== -1){
|
||||
prev.splice(existingIndex, 1);
|
||||
return [...prev];
|
||||
}
|
||||
|
||||
}
|
||||
prev[existingIndex] = {...prev[existingIndex], ...cur};
|
||||
return [...prev];
|
||||
}, callbacks);
|
||||
|
||||
updated.sort( (a, b) => a.display_id > b.display_id ? -1 : 1);
|
||||
setCallbacks(updated);
|
||||
}
|
||||
prev[existingIndex] = {...prev[existingIndex], ...cur};
|
||||
return [...prev];
|
||||
}, callbacks.current);
|
||||
updated.sort( (a, b) => a.display_id > b.display_id ? -1 : 1);
|
||||
callbacks.current = updated;
|
||||
},
|
||||
onError: ({data}) => {
|
||||
console.log(data)
|
||||
},
|
||||
});
|
||||
useQuery(Query_Callbacks_And_Edges, {
|
||||
fetchPolicy: "no-cache",
|
||||
onCompleted: (data) => {
|
||||
const updated = data.callback.reduce( (prev, cur) => {
|
||||
let existingIndex = prev.findIndex( (element, i, array) => element.id === cur.id);
|
||||
if(existingIndex === -1){
|
||||
// cur isn't in our current list of callbacks
|
||||
if(cur.active){return [...prev, cur]}
|
||||
}
|
||||
if(!cur.active){
|
||||
if(existingIndex !== -1){
|
||||
prev.splice(existingIndex, 1);
|
||||
return [...prev];
|
||||
}
|
||||
|
||||
}
|
||||
prev[existingIndex] = {...prev[existingIndex], ...cur};
|
||||
return [...prev];
|
||||
}, callbacks.current);
|
||||
updated.sort( (a, b) => a.display_id > b.display_id ? -1 : 1);
|
||||
callbacks.current = updated;
|
||||
callbackEdges.current = data.callbackgraphedge;
|
||||
setLoading(false);
|
||||
}
|
||||
})
|
||||
useSubscription(SUB_Edges, {
|
||||
fetchPolicy: "network-only",
|
||||
variables: {fromNow: fromNow.current},
|
||||
onData: ({data}) => {
|
||||
if(!mountedRef.current){
|
||||
return;
|
||||
}
|
||||
setCallbackEdges(data.data.callbackgraphedge)
|
||||
const updated = data.data.callbackgraphedge_stream.reduce( (prev, cur) => {
|
||||
let existingIndex = prev.findIndex( (element, i, array) => element.id === cur.id);
|
||||
if(existingIndex === -1){
|
||||
// cur isn't in our current list of callbacks
|
||||
return [...prev, cur]
|
||||
}
|
||||
prev[existingIndex] = {...prev[existingIndex], ...cur};
|
||||
return [...prev];
|
||||
}, callbackEdges.current);
|
||||
callbackEdges.current = updated;
|
||||
}
|
||||
});
|
||||
const onOpenTabLocal = React.useCallback( ({tabType, tabID, callbackID}) => {
|
||||
for(let i = 0; i < callbacks.length; i++){
|
||||
if(callbacks[i]["id"] === callbackID){
|
||||
const onOpenTabLocal = React.useCallback( ({tabType, tabID, callbackID, customBrowser}) => {
|
||||
for(let i = 0; i < callbacks.current.length; i++){
|
||||
if(callbacks.current[i]["id"] === callbackID){
|
||||
const tabData = {tabID, tabType, callbackID,
|
||||
displayID: callbacks[i]["display_id"],
|
||||
payloadtype: callbacks[i]["payload"]["payloadtype"]["name"],
|
||||
payloadtype_id: callbacks[i]["payload"]["payloadtype"]["id"],
|
||||
operation_id: callbacks[i]["operation_id"],
|
||||
payloadDescription: callbacks[i]["payload"]["description"],
|
||||
callbackDescription: callbacks[i]["description"],
|
||||
host: callbacks[i]["host"],
|
||||
os: callbacks[i]["payload"]["os"]};
|
||||
displayID: callbacks.current[i]["display_id"],
|
||||
payloadtype: callbacks.current[i]["payload"]["payloadtype"]["name"],
|
||||
payloadtype_id: callbacks.current[i]["payload"]["payloadtype"]["id"],
|
||||
operation_id: callbacks.current[i]["operation_id"],
|
||||
payloadDescription: callbacks.current[i]["payload"]["description"],
|
||||
callbackDescription: callbacks.current[i]["description"],
|
||||
host: callbacks.current[i]["host"],
|
||||
color: callbacks.current[i]["color"],
|
||||
os: callbacks.current[i]["payload"]["os"],
|
||||
customBrowser: customBrowser,
|
||||
};
|
||||
props.onOpenTab(tabData);
|
||||
}
|
||||
}
|
||||
}, [callbacks, props.onOpenTab]);
|
||||
}, [callbacks.current, props.onOpenTab]);
|
||||
const onOpenTabsLocal = React.useCallback( ({tabs}) => {
|
||||
let newTabData = [];
|
||||
for(let j = 0; j < tabs.length; j++){
|
||||
for(let i = 0; i < callbacks.current.length; i++){
|
||||
if(callbacks.current[i]["id"] === tabs[j].callbackID){
|
||||
const tabData = {
|
||||
tabID: tabs[j].tabID,
|
||||
tabType: tabs[j].tabType,
|
||||
callbackID: tabs[j].callbackID,
|
||||
displayID: callbacks.current[i]["display_id"],
|
||||
payloadtype: callbacks.current[i]["payload"]["payloadtype"]["name"],
|
||||
payloadtype_id: callbacks.current[i]["payload"]["payloadtype"]["id"],
|
||||
operation_id: callbacks.current[i]["operation_id"],
|
||||
payloadDescription: callbacks.current[i]["payload"]["description"],
|
||||
callbackDescription: callbacks.current[i]["description"],
|
||||
host: callbacks.current[i]["host"],
|
||||
color: callbacks.current[i]["color"],
|
||||
os: callbacks.current[i]["payload"]["os"],
|
||||
customBrowser: tabs[j].customBrowser,
|
||||
};
|
||||
newTabData.push(tabData);
|
||||
}
|
||||
}
|
||||
}
|
||||
props.onOpenTabs(newTabData);
|
||||
}, [callbacks.current, props.onOpenTabs]);
|
||||
React.useEffect( () => {
|
||||
return() => {
|
||||
mountedRef.current = false;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
const initialNewBrowserScriptTable = useMythicSetting({setting_name: "experiment-browserscripttable", default_value: "false"});
|
||||
return (
|
||||
<div style={{height: "100%", width: "100%"}}>
|
||||
<CallbackGraphEdgesContext.Provider value={callbackEdges}>
|
||||
<CallbackGraphEdgesContext.Provider value={callbackEdges.current}>
|
||||
<OnOpenTabContext.Provider value={onOpenTabLocal}>
|
||||
<CallbacksContext.Provider value={callbacks}>
|
||||
{props.topDisplay === "graph" ? (
|
||||
<CallbacksGraph maxHeight={"100%"} />
|
||||
) : initialNewBrowserScriptTable ? (
|
||||
<CallbacksTableMaterialReactTable onOpenTab={onOpenTabLocal}
|
||||
parentMountedRef={mountedRef} me={me}/>
|
||||
) : (
|
||||
<CallbacksTable onOpenTab={onOpenTabLocal}
|
||||
parentMountedRef={mountedRef} me={me}/>
|
||||
)
|
||||
}
|
||||
</CallbacksContext.Provider>
|
||||
<OnOpenTabsContext.Provider value={onOpenTabsLocal}>
|
||||
<CallbacksContext.Provider value={callbacks.current}>
|
||||
{props.topDisplay === "graph" ? (
|
||||
<CallbacksGraph onOpenTab={onOpenTabLocal} maxHeight={"100%"} clickedTabId={props.clickedTabId} />
|
||||
) : (
|
||||
<CallbacksTable callbackTableGridRef={props.callbackTableGridRef}
|
||||
parentMountedRef={mountedRef} me={me}
|
||||
loading={loading}
|
||||
clickedTabId={props.clickedTabId}/>
|
||||
)
|
||||
}
|
||||
</CallbacksContext.Provider>
|
||||
</OnOpenTabsContext.Provider>
|
||||
</OnOpenTabContext.Provider>
|
||||
</CallbackGraphEdgesContext.Provider>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,6 @@ import {useTheme} from '@mui/material/styles';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import Button from '@mui/material/Button';
|
||||
import {ExpandedCallbackSideDetailsTable} from '../ExpandedCallback/ExpandedCallbackSideDetails';
|
||||
import { toLocalTime } from '../../utilities/Time';
|
||||
import {PayloadsTableRowBuildProcessPerStep} from '../Payloads/PayloadsTableRowBuildProgress';
|
||||
@@ -24,17 +23,36 @@ import {AddRemoveCallbackCommandsDialog} from './AddRemoveCallbackCommandsDialog
|
||||
import { snackActions } from '../../utilities/Snackbar';
|
||||
import Box from '@mui/material/Box';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import {ParseForDisplay} from "../Payloads/DetailedPayloadTable";
|
||||
import {DetailedPayloadTable, ParseForDisplay} from "../Payloads/DetailedPayloadTable";
|
||||
import {Button, Link, IconButton} from '@mui/material';
|
||||
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
|
||||
import MenuBookIcon from '@mui/icons-material/MenuBook';
|
||||
import InfoIconOutline from '@mui/icons-material/InfoOutlined';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import {HostFileDialog} from "../Payloads/HostFileDialog";
|
||||
import PublicIcon from '@mui/icons-material/Public';
|
||||
import {payloadsCallbackAllowed} from "../Payloads/Payloads";
|
||||
import Switch from '@mui/material/Switch';
|
||||
|
||||
const GET_Payload_Details = gql`
|
||||
query GetCallbackDetails($callback_id: Int!) {
|
||||
callback_by_pk(id: $callback_id){
|
||||
tags {
|
||||
tagtype {
|
||||
name
|
||||
color
|
||||
id
|
||||
}
|
||||
id
|
||||
}
|
||||
payload {
|
||||
uuid
|
||||
id
|
||||
creation_time
|
||||
callback_allowed
|
||||
payloadtype{
|
||||
name
|
||||
agent_type
|
||||
id
|
||||
}
|
||||
filemetum {
|
||||
@@ -44,6 +62,23 @@ query GetCallbackDetails($callback_id: Int!) {
|
||||
md5
|
||||
sha1
|
||||
}
|
||||
operator {
|
||||
username
|
||||
}
|
||||
eventstepinstance{
|
||||
id
|
||||
eventgroupinstance {
|
||||
id
|
||||
eventgroup {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
eventstep {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
payload_build_steps(order_by: {step_number: asc}) {
|
||||
step_name
|
||||
step_number
|
||||
@@ -88,6 +123,9 @@ query GetCallbackDetails($callback_id: Int!) {
|
||||
cmd
|
||||
id
|
||||
version
|
||||
payloadtype {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
architecture
|
||||
@@ -118,6 +156,8 @@ query GetCallbackDetails($callback_id: Int!) {
|
||||
process_name
|
||||
init_callback
|
||||
mythictree_groups
|
||||
cwd
|
||||
impersonation_context
|
||||
}
|
||||
|
||||
}
|
||||
@@ -145,6 +185,8 @@ mutation removeLoadedCommand($id: Int!){
|
||||
export function DetailedCallbackTable(props){
|
||||
const theme = useTheme();
|
||||
const me = useReactiveVar(meState);
|
||||
const [openHostDialog, setOpenHostDialog] = React.useState(false);
|
||||
const [openDetailedView, setOpenDetailedView] = React.useState(false);
|
||||
const [openAddRemoveCommandsDialog, setOpenAddRemoveCommandsDialog] = React.useState(false);
|
||||
const [commands, setCommands] = React.useState([]);
|
||||
const [buildParameters, setBuildParameters] = React.useState([]);
|
||||
@@ -181,6 +223,7 @@ export function DetailedCallbackTable(props){
|
||||
issueNextMod();
|
||||
}
|
||||
})
|
||||
const [payloadCallbackAllowed, setPayloadCallbackAllowed] = React.useState(true);
|
||||
const issueNextMod = () => {
|
||||
if(commandMods.current.add >= addTotal.current){
|
||||
if(commandMods.current.remove >= removeTotal.current) {
|
||||
@@ -221,7 +264,7 @@ export function DetailedCallbackTable(props){
|
||||
onCompleted: data => {
|
||||
const commandState = data.callback_by_pk.loadedcommands.map( (c) =>
|
||||
{
|
||||
return {cmd: c.command.cmd, mythic: c.command.version, payload: c.version}
|
||||
return {cmd: c.command.cmd, mythic: c.command.version, payload: c.version, payload_type: c.command.payloadtype.name}
|
||||
}).sort((a,b) => (a.cmd > b.cmd) ? 1: ((b.cmd > a.cmd) ? -1 : 0));
|
||||
setCommands(commandState);
|
||||
const buildParametersState = data.callback_by_pk.payload.buildparameterinstances.map( (b) =>
|
||||
@@ -257,8 +300,29 @@ export function DetailedCallbackTable(props){
|
||||
}];
|
||||
}, []);
|
||||
setC2Profiles(c2ProfilesState);
|
||||
setPayloadCallbackAllowed(data.callback_by_pk.payload.callback_allowed);
|
||||
}
|
||||
});
|
||||
const [toggleCallbackAllowedMutation] = useMutation(payloadsCallbackAllowed, {
|
||||
onCompleted: (data) => {
|
||||
if(data.updatePayload.status === "success"){
|
||||
setPayloadCallbackAllowed(data.updatePayload.callback_allowed);
|
||||
if(data.updatePayload.callback_allowed){
|
||||
snackActions.success("New Callbacks allowed from this Payload");
|
||||
} else {
|
||||
snackActions.warning("No new Callback allowed from this Payload");
|
||||
}
|
||||
} else {
|
||||
console.log(data.updatePayload);
|
||||
}
|
||||
},
|
||||
onError: (data) => {
|
||||
console.log(data);
|
||||
}
|
||||
});
|
||||
const onToggleCallbackAllowed = () => {
|
||||
toggleCallbackAllowedMutation({variables: {payload_uuid: data.callback_by_pk.payload.uuid, callback_allowed: !payloadCallbackAllowed}})
|
||||
}
|
||||
if (loading) {
|
||||
return <LinearProgress style={{marginTop: "10px"}}/>;
|
||||
}
|
||||
@@ -300,7 +364,19 @@ export function DetailedCallbackTable(props){
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>UUID</TableCell>
|
||||
<TableCell>{data.callback_by_pk.payload.uuid}</TableCell>
|
||||
<TableCell>
|
||||
{data.callback_by_pk.payload.uuid}
|
||||
<IconButton disableFocusRipple={true}
|
||||
disableRipple={true} size="small" color="info" onClick={() => setOpenDetailedView(true)}>
|
||||
<InfoIconOutline />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
{openDetailedView ? (
|
||||
<MythicDialog fullWidth={true} maxWidth="lg" open={openDetailedView}
|
||||
onClose={()=>{setOpenDetailedView(false);}}
|
||||
innerDialog={<DetailedPayloadTable {...props} payload_id={data.callback_by_pk.payload.id} onClose={()=>{setOpenDetailedView(false);}} />}
|
||||
/>
|
||||
) : null }
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>Creation Time</TableCell>
|
||||
@@ -314,7 +390,22 @@ export function DetailedCallbackTable(props){
|
||||
) : null }
|
||||
<TableRow hover>
|
||||
<TableCell>Download URL</TableCell>
|
||||
<TableCell>{window.location.origin + "/direct/download/" + data.callback_by_pk.payload.filemetum.agent_file_id}</TableCell>
|
||||
<TableCell style={{display: "flex", alignItems: "center"}}>
|
||||
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" href={"/direct/download/" + data.callback_by_pk.payload.filemetum.agent_file_id}>
|
||||
{window.location.origin + "/direct/download/" + data.callback_by_pk.payload.filemetum.agent_file_id}
|
||||
</Link>
|
||||
<MythicStyledTooltip title={"Host Payload Through C2"} >
|
||||
<PublicIcon color={"info"} style={{marginLeft: "20px", cursor: "pointer"}} onClick={()=>{setOpenHostDialog(true);}} />
|
||||
</MythicStyledTooltip>
|
||||
{openHostDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openHostDialog}
|
||||
onClose={()=>{setOpenHostDialog(false);}}
|
||||
innerDialog={<HostFileDialog file_uuid={data.callback_by_pk.payload.filemetum.agent_file_id}
|
||||
file_name={b64DecodeUnicode(data.callback_by_pk.payload.filemetum.filename_text)}
|
||||
onClose={()=>{setOpenHostDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>SHA1</TableCell>
|
||||
@@ -324,6 +415,40 @@ export function DetailedCallbackTable(props){
|
||||
<TableCell>MD5</TableCell>
|
||||
<TableCell>{data.callback_by_pk.payload.filemetum.md5}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>Created By</TableCell>
|
||||
<TableCell>{data.callback_by_pk.payload?.operator?.username}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>New Callbacks Allowed?</TableCell>
|
||||
<TableCell>
|
||||
<Switch
|
||||
checked={payloadCallbackAllowed}
|
||||
onChange={onToggleCallbackAllowed}
|
||||
color="info"
|
||||
inputProps={{'aria-label': 'primary checkbox'}}
|
||||
name="callback_allowed"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{data.callback_by_pk.payload?.eventstepinstance &&
|
||||
<>
|
||||
<TableRow hover>
|
||||
<TableCell>Generated via Eventing</TableCell>
|
||||
<TableCell>
|
||||
<Link color="textPrimary" underline="always"
|
||||
href={"/new/eventing?eventgroup=" +
|
||||
data.callback_by_pk.payload?.eventstepinstance?.eventgroupinstance?.eventgroup.id +
|
||||
"&eventgroupinstance=" +
|
||||
data.callback_by_pk.payload?.eventstepinstance?.eventgroupinstance?.id
|
||||
}>
|
||||
{data.callback_by_pk.payload?.eventstepinstance?.eventgroupinstance?.eventgroup?.name} -
|
||||
{data.callback_by_pk.payload?.eventstepinstance?.eventstep?.name}
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
|
||||
@@ -353,10 +478,10 @@ export function DetailedCallbackTable(props){
|
||||
): (cmd.value)
|
||||
)
|
||||
}
|
||||
{cmd.enc_key === null ? (null) : (<React.Fragment>
|
||||
{cmd.enc_key === null ? null : (<React.Fragment>
|
||||
<br/><b>Encryption Key: </b> {cmd.enc_key}
|
||||
</React.Fragment>) }
|
||||
{cmd.dec_key === null ? (null) : (<React.Fragment>
|
||||
{cmd.dec_key === null ? null : (<React.Fragment>
|
||||
<br/><b>Decryption Key: </b> {cmd.dec_key}
|
||||
</React.Fragment>) }
|
||||
</TableCell>
|
||||
@@ -417,10 +542,10 @@ export function DetailedCallbackTable(props){
|
||||
<TableCell>{cmd.description}</TableCell>
|
||||
<TableCell>
|
||||
<ParseForDisplay cmd={cmd} />
|
||||
{cmd.enc_key === null ? (null) : (<React.Fragment>
|
||||
{cmd.enc_key === null ? null : (<React.Fragment>
|
||||
<br/><b>Encryption Key: </b> {cmd.enc_key}
|
||||
</React.Fragment>) }
|
||||
{cmd.dec_key === null ? (null) : (<React.Fragment>
|
||||
{cmd.dec_key === null ? null : (<React.Fragment>
|
||||
<br/><b>Decryption Key: </b> {cmd.dec_key}
|
||||
</React.Fragment>) }
|
||||
</TableCell>
|
||||
@@ -441,6 +566,7 @@ export function DetailedCallbackTable(props){
|
||||
<Table size="small" aria-label="details" style={{"overflowWrap": "break-word"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{width: "40px"}}>Payload</TableCell>
|
||||
<TableCell>Command Name</TableCell>
|
||||
<TableCell>Mythic Version</TableCell>
|
||||
<TableCell>Loaded Version</TableCell>
|
||||
@@ -450,22 +576,27 @@ export function DetailedCallbackTable(props){
|
||||
<TableBody>
|
||||
{
|
||||
commands.map( (cmd) => (
|
||||
<TableRow key={cmd.cmd + data.callback_by_pk.payload.id} hover>
|
||||
<TableCell>{cmd.cmd}</TableCell>
|
||||
<TableRow key={cmd.cmd + cmd.payload_type} hover>
|
||||
<TableCell>
|
||||
<MythicAgentSVGIcon payload_type={cmd.payload_type} style={{width: "40px"}}/>
|
||||
</TableCell>
|
||||
<TableCell>{cmd.cmd} ({cmd.payload_type})</TableCell>
|
||||
<TableCell>{cmd.mythic}</TableCell>
|
||||
<TableCell>{cmd.payload}</TableCell>
|
||||
<TableCell>
|
||||
<Button variant="contained" color="primary" target="_blank"
|
||||
href={"/docs/agents/" + data.callback_by_pk.payload.payloadtype.name + "/commands/" + cmd.cmd}>Docs</Button>
|
||||
<IconButton variant="contained" target="_blank"
|
||||
href={"/docs/agents/" + cmd.payload_type + "/commands/" + cmd.cmd}>
|
||||
<MenuBookIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
|
||||
}
|
||||
{openAddRemoveCommandsDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openAddRemoveCommandsDialog}
|
||||
onClose={()=>{setOpenAddRemoveCommandsDialog(false);}}
|
||||
innerDialog={<AddRemoveCallbackCommandsDialog callback_id={props.callback_id}
|
||||
innerDialog={<AddRemoveCallbackCommandsDialog
|
||||
callback_id={props.callback_id}
|
||||
display_id={props.display_id} onClose={()=>{setOpenAddRemoveCommandsDialog(false);}} onSubmit={addRemoveCommandsSubmit} />}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import GetAppIcon from '@mui/icons-material/GetApp';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import {Typography, Link} from '@mui/material';
|
||||
import {Button, Link, Typography} from '@mui/material';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import {MythicDialog} from "../../MythicComponents/MythicDialog";
|
||||
import {PreviewFileMediaDialog} from "../Search/PreviewFileMedia";
|
||||
import {faPhotoVideo} from '@fortawesome/free-solid-svg-icons';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
|
||||
export function DownloadHistoryDialog(props){
|
||||
const [history, setHistory] = React.useState([]);
|
||||
const theme = useTheme();
|
||||
|
||||
const [openPreviewMediaDialog, setOpenPreviewMediaDialog] = React.useState(false);
|
||||
const previewMediaRef = React.useRef();
|
||||
const onPreviewMedia = (event, hist) => {
|
||||
if(event){
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
setOpenPreviewMediaDialog(true);
|
||||
previewMediaRef.current = hist;
|
||||
}
|
||||
useEffect( () => {
|
||||
setHistory(props.value);
|
||||
}, [props.value]);
|
||||
@@ -25,42 +33,70 @@ export function DownloadHistoryDialog(props){
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">{props.title}</DialogTitle>
|
||||
|
||||
<TableContainer component={Paper} className="mythicElement">
|
||||
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
|
||||
<TableContainer className="mythicElement">
|
||||
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{width: "5rem"}}>Download</TableCell>
|
||||
<TableCell>Time</TableCell>
|
||||
<TableCell>Task</TableCell>
|
||||
<TableCell>Callback</TableCell>
|
||||
<TableCell style={{}}></TableCell>
|
||||
<TableCell style={{width: "15rem"}}>Time</TableCell>
|
||||
<TableCell style={{width: "7rem"}}>Task</TableCell>
|
||||
<TableCell style={{width: "7rem"}}>Callback</TableCell>
|
||||
<TableCell>Comment</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{history.map( (hist) => (
|
||||
<TableRow key={'hist' + hist.id}>
|
||||
<TableCell >{hist.complete ? (
|
||||
<IconButton
|
||||
href={"/api/v1.4/files/download/" + hist.agent_file_id}
|
||||
style={{color: theme.palette.success.main}}
|
||||
size="large"><GetAppIcon /></IconButton>
|
||||
) : (hist.chunks_received + "/" + hist.total_chunks)}</TableCell>
|
||||
<TableCell >
|
||||
{hist.complete ? (
|
||||
<div style={{display: "inline-flex", alignItems: "center"}}>
|
||||
<MythicStyledTooltip title={"Preview Media"}>
|
||||
<FontAwesomeIcon icon={faPhotoVideo} style={{
|
||||
height: "25px", marginRight: "5px", position: "relative", cursor: "pointer", display: "inline-block"}}
|
||||
onClick={(e) => onPreviewMedia(e, hist)} />
|
||||
</MythicStyledTooltip>
|
||||
|
||||
{hist.deleted ? (
|
||||
<Typography variant="body2" style={{wordBreak: "break-all"}}>{hist.full_remote_path_text === "" ? hist.filename_text : hist.full_remote_path_text}</Typography>
|
||||
) : (
|
||||
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" href={"/direct/download/" + hist.agent_file_id}>{hist.full_remote_path_text === "" ? hist.filename_text : hist.full_remote_path_text}</Link>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
) : (hist.chunks_received + "/" + hist.total_chunks)}
|
||||
</TableCell>
|
||||
<TableCell>{hist.timestamp}</TableCell>
|
||||
<TableCell>
|
||||
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
|
||||
href={"/new/task/" + hist.task.display_id}>
|
||||
{hist.task.display_id}
|
||||
</Link>
|
||||
{hist.task &&
|
||||
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
|
||||
href={"/new/task/" + hist.task.display_id}>
|
||||
T-{hist.task.display_id}
|
||||
</Link>
|
||||
}
|
||||
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
|
||||
href={"/new/callbacks/" + hist.task.callback.display_id}>
|
||||
{hist.task.callback.display_id}
|
||||
</Link>
|
||||
{hist.task &&
|
||||
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
|
||||
href={"/new/callbacks/" + hist.task.callback.display_id}>
|
||||
C-{hist.task.callback.display_id}
|
||||
</Link>
|
||||
}
|
||||
|
||||
</TableCell>
|
||||
<TableCell>{hist.comment}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{openPreviewMediaDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openPreviewMediaDialog}
|
||||
onClose={(e)=>{setOpenPreviewMediaDialog(false);}}
|
||||
innerDialog={<PreviewFileMediaDialog
|
||||
agent_file_id={previewMediaRef.current?.agent_file_id}
|
||||
filename={previewMediaRef.current?.filename_text}
|
||||
onClose={(e)=>{setOpenPreviewMediaDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
+5
-2
@@ -26,6 +26,7 @@ import LayersIcon from '@mui/icons-material/Layers';
|
||||
const getCallbackMythicTreeGroups = gql`
|
||||
query getCallbackMythicTreeGroups($callback_id: Int!) {
|
||||
callback_by_pk(id: $callback_id){
|
||||
display_id
|
||||
mythictree_groups
|
||||
}
|
||||
callback {
|
||||
@@ -57,12 +58,14 @@ export function ModifyCallbackMythicTreeGroupsDialog(props){
|
||||
onError: error => {
|
||||
snackActions.error(error.message);
|
||||
}
|
||||
})
|
||||
});
|
||||
const callbackDisplayID = React.useRef(0);
|
||||
const { data } = useQuery(getCallbackMythicTreeGroups, {
|
||||
fetchPolicy: "no-cache",
|
||||
variables: {callback_id: props.callback_id},
|
||||
onCompleted: data => {
|
||||
setGroups(data.callback_by_pk.mythictree_groups);
|
||||
callbackDisplayID.current = data.callback_by_pk.display_id;
|
||||
let otherGroupOptions = new Set([]);
|
||||
for(let i = 0; i < data.callback.length; i++){
|
||||
if(data.callback[i].mythictree_groups.length > 0){
|
||||
@@ -109,7 +112,7 @@ export function ModifyCallbackMythicTreeGroupsDialog(props){
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title" style={{display: "flex", justifyContent: "space-between"}}>
|
||||
Updating Callback Groups for Callback {props.callback_id}
|
||||
Updating Callback Groups for Callback {callbackDisplayID.current}
|
||||
<MythicStyledTooltip title="View all groups" >
|
||||
<IconButton size="small" onClick={()=>{setOpenViewAllCallbacksDialog(true);}} style={{color: theme.palette.info.main}} variant="contained"><LayersIcon/></IconButton>
|
||||
</MythicStyledTooltip>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import {gql, useLazyQuery, useReactiveVar, useSubscription} from '@apollo/client';
|
||||
import {meState} from '../../../cache';
|
||||
import {gql, useLazyQuery, useSubscription} from '@apollo/client';
|
||||
import {snackActions} from '../../utilities/Snackbar';
|
||||
import {ResponseDisplayScreenshot} from './ResponseDisplayScreenshot';
|
||||
import {ResponseDisplayPlaintext} from './ResponseDisplayPlaintext';
|
||||
@@ -15,16 +14,16 @@ import {MythicStyledTooltip} from '../../MythicComponents/MythicStyledTooltip';
|
||||
import Pagination from '@mui/material/Pagination';
|
||||
import {ResponseDisplayInteractive} from "./ResponseDisplayInteractive";
|
||||
import {ResponseDisplayMedia} from "./ResponseDisplayMedia";
|
||||
import {ResponseDisplayMaterialReactTable} from "./ResponseDisplayMaterialReactTable";
|
||||
import {useMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {DrawBrowserScriptElementsFlow} from "./C2PathDialog";
|
||||
import {GetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {ResponseDisplayGraph} from "./ResponseDisplayGraph";
|
||||
import {operatorSettingDefaults} from "../../../cache";
|
||||
import {ResponseDisplayTabs} from "./ResponseDisplayTabs";
|
||||
|
||||
const subResponsesStream = gql`
|
||||
subscription subResponsesStream($task_id: Int!){
|
||||
response_stream(
|
||||
batch_size: 50,
|
||||
cursor: {initial_value: {id: 0}},
|
||||
cursor: {initial_value: {timestamp: "1970-01-01"}},
|
||||
where: {task_id: {_eq: $task_id} }
|
||||
){
|
||||
id
|
||||
@@ -62,8 +61,8 @@ query subResponsesQuery($task_id: Int!, $search: String!) {
|
||||
}
|
||||
}`;
|
||||
const taskScript = gql`
|
||||
query getBrowserScriptsQuery($command_id: Int!, $operator_id: Int!){
|
||||
browserscript(where: {active: {_eq: true}, command_id: {_eq: $command_id}, for_new_ui: {_eq: true}, operator_id: {_eq: $operator_id}}) {
|
||||
query getBrowserScriptsQuery($command_id: Int!){
|
||||
browserscript(where: {active: {_eq: true}, command_id: {_eq: $command_id}, for_new_ui: {_eq: true}}) {
|
||||
script
|
||||
id
|
||||
}
|
||||
@@ -80,7 +79,7 @@ export function b64DecodeUnicode(str) {
|
||||
bytes[i] = text.charCodeAt(i);
|
||||
}
|
||||
const decoder = new TextDecoder(); // default is utf-8
|
||||
return decodeURIComponent(decoder.decode(bytes));
|
||||
return decoder.decode(bytes);
|
||||
}catch(error){
|
||||
try{
|
||||
return decodeURIComponent(window.atob(str));
|
||||
@@ -98,24 +97,30 @@ export const ResponseDisplay = (props) =>{
|
||||
const interactive = props?.task?.command?.supported_ui_features.includes("task_response:interactive") || false;
|
||||
return (
|
||||
interactive ? (
|
||||
<ResponseDisplayInteractive {...props} />
|
||||
<ResponseDisplayInteractive {...props} key={props?.task?.id} />
|
||||
) : (
|
||||
<NonInteractiveResponseDisplay {...props} />
|
||||
<NonInteractiveResponseDisplay {...props} key={props?.task?.id} />
|
||||
)
|
||||
)
|
||||
}
|
||||
const NonInteractiveResponseDisplay = (props) => {
|
||||
const theme = useTheme();
|
||||
const [output, setOutput] = React.useState("");
|
||||
const [rawResponses, setRawResponses] = React.useState([]);
|
||||
const taskID = React.useRef(props.task.id);
|
||||
const seenResponseIDs = React.useRef([]);
|
||||
const search = React.useRef("");
|
||||
const [totalCount, setTotalCount] = React.useState(0);
|
||||
const [openBackdrop, setOpenBackdrop] = React.useState(true);
|
||||
const initialResponseStreamLimit = useMythicSetting({setting_name: "experiment-responseStreamLimit", default_value: 10, output: "number"})
|
||||
const togglingAllOutputToPaginated = React.useRef(false);
|
||||
const initialResponseStreamLimit = GetMythicSetting({setting_name: "experiment-responseStreamLimit", default_value: operatorSettingDefaults["experiment-responseStreamLimit"]});
|
||||
const [fetchMoreResponses] = useLazyQuery(getResponsesLazyQuery, {
|
||||
fetchPolicy: "network-only",
|
||||
onCompleted: (data) => {
|
||||
//console.log("fetchMoreResponses called", data)
|
||||
data.response.forEach( (r) => {
|
||||
if(!seenResponseIDs.current.includes(r.id)){
|
||||
seenResponseIDs.current.push(r.id);
|
||||
}
|
||||
})
|
||||
// set raw responses to be what we just manually fetched
|
||||
const responseArray = data.response.map( r =>{ return {...r, response: b64DecodeUnicode(r.response)}});
|
||||
setRawResponses(responseArray);
|
||||
@@ -125,7 +130,6 @@ const NonInteractiveResponseDisplay = (props) => {
|
||||
}, b64DecodeUnicode(""));
|
||||
setOutput(responses);
|
||||
// update maxID
|
||||
|
||||
if(!props.selectAllOutput){
|
||||
setTotalCount(data.response_aggregate.aggregate.count);
|
||||
}
|
||||
@@ -148,6 +152,7 @@ const NonInteractiveResponseDisplay = (props) => {
|
||||
|
||||
setTotalCount(1);
|
||||
setOpenBackdrop(false);
|
||||
togglingAllOutputToPaginated.current = true;
|
||||
},
|
||||
onError: (data) => {
|
||||
|
||||
@@ -163,52 +168,60 @@ const NonInteractiveResponseDisplay = (props) => {
|
||||
}else{
|
||||
fetchAllResponses({variables: {task_id: props.task.id, search: "%" + search.current + "%"}})
|
||||
}
|
||||
}else{
|
||||
}else if(togglingAllOutputToPaginated.current){
|
||||
// going from select all output to not select all output
|
||||
// don't fetch this on first load
|
||||
onSubmitPageChange(1);
|
||||
togglingAllOutputToPaginated.current = false;
|
||||
}
|
||||
//}
|
||||
}, [props.selectAllOutput]);
|
||||
}, [props.selectAllOutput, togglingAllOutputToPaginated.current]);
|
||||
React.useEffect( () => {
|
||||
setOpenBackdrop(true);
|
||||
onSubmitPageChange(1);
|
||||
setOutput("");
|
||||
setRawResponses([]);
|
||||
setTotalCount(0);
|
||||
//onSubmitPageChange(1);
|
||||
}, [props.task.id]);
|
||||
const subscriptionDataCallback = React.useCallback( ({data}) => {
|
||||
setTimeout(() => {
|
||||
// close the backdrop after 2 seconds in case there's no data to fetch
|
||||
setOpenBackdrop(false);
|
||||
}, 1000);
|
||||
const subscriptionDataCallback = ({data}) => {
|
||||
//console.log("fetchLimit", fetchLimit, "totalCount", totalCount);
|
||||
if(props.task.id !== taskID.current){
|
||||
console.log("props.task.id !== taskID.current", props.task.id, taskID.current)
|
||||
taskID.current = props.task.id;
|
||||
// this is the latest batch of responses
|
||||
|
||||
// base64 decode all of the response data
|
||||
const responseArray = data.data.response_stream.map( r =>{ return {...r, response: b64DecodeUnicode(r.response)}});
|
||||
// set the aggregated output
|
||||
const responses = responseArray.reduce( (prev, cur) => {
|
||||
return prev + cur.response;
|
||||
}, b64DecodeUnicode(""));
|
||||
setOutput(responses);
|
||||
|
||||
setRawResponses(responseArray);
|
||||
setTotalCount(responseArray.length);
|
||||
setOpenBackdrop(false);
|
||||
} else {
|
||||
if(totalCount >= initialResponseStreamLimit && initialResponseStreamLimit > 0){
|
||||
if(rawResponses.length >= initialResponseStreamLimit && initialResponseStreamLimit > 0 && !props.selectAllOutput){
|
||||
// we won't display it
|
||||
console.log("got more than we can see currently", totalCount);
|
||||
setOpenBackdrop(false);
|
||||
let newTotal = totalCount;
|
||||
data.data.response_stream.forEach( (r) => {
|
||||
if(!seenResponseIDs.current.includes(r.id)){
|
||||
newTotal += 1;
|
||||
seenResponseIDs.current.push(r.id);
|
||||
}
|
||||
})
|
||||
setTotalCount(newTotal);
|
||||
return;
|
||||
}
|
||||
// we still have some room to view more, but only room for initialResponseStreamLimit - totalFetched.current
|
||||
const currentIDS = rawResponses.map( r => r.id);
|
||||
let newTotal = totalCount;
|
||||
const newerResponses = data.data.response_stream.reduce( (prev, cur) => {
|
||||
if(currentIDS.includes(cur.id)){return prev}
|
||||
if(!seenResponseIDs.current.includes(cur.id)){
|
||||
newTotal += 1;
|
||||
seenResponseIDs.current.push(cur.id);
|
||||
}
|
||||
let prevIndex = prev.findIndex( (v,i,a) => v.id === cur.id);
|
||||
if(prevIndex >= 0){
|
||||
prev[prevIndex] = {...cur, response: b64DecodeUnicode(cur.response)};
|
||||
return prev;
|
||||
}
|
||||
return [...prev, {...cur, response: b64DecodeUnicode(cur.response)}]
|
||||
}, rawResponses);
|
||||
// sort them to make sure we're still in order
|
||||
newerResponses.sort( (a,b) => a.id > b.id ? 1 : -1);
|
||||
setTotalCount(newTotal);
|
||||
// newerResponses is everything we've seen plus everything new
|
||||
if(initialResponseStreamLimit > 0){
|
||||
if(initialResponseStreamLimit > 0 && !props.selectAllOutput){
|
||||
// take just the responses that make up our stream limit
|
||||
const finalRawResponses = newerResponses.slice(0, initialResponseStreamLimit);
|
||||
const outputResponses = finalRawResponses.reduce( (prev, cur) => {
|
||||
@@ -226,8 +239,8 @@ const NonInteractiveResponseDisplay = (props) => {
|
||||
setOutput(outputResponses);
|
||||
}
|
||||
setOpenBackdrop(false);
|
||||
}
|
||||
}, [output, rawResponses, totalCount, props.task.id]);
|
||||
|
||||
};
|
||||
useSubscription(subResponsesStream, {
|
||||
variables: {task_id: props.task.id},
|
||||
fetchPolicy: "network_only",
|
||||
@@ -279,19 +292,22 @@ const NonInteractiveResponseDisplay = (props) => {
|
||||
}, [search.current]);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<Backdrop open={openBackdrop} onClick={()=>{setOpenBackdrop(false);}} style={{zIndex: 2, position: "absolute"}}>
|
||||
<CircularProgress color="inherit" disableShrink />
|
||||
</Backdrop>
|
||||
{props.searchOutput &&
|
||||
<SearchBar onSubmitSearch={onSubmitSearch} />
|
||||
}
|
||||
{!openBackdrop &&
|
||||
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%"}}>
|
||||
|
||||
{!openBackdrop &&
|
||||
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%",
|
||||
backgroundColor: theme.outputBackgroundColor + (theme.palette.mode === 'dark' ? "D0" : "D0"),
|
||||
color: theme.outputTextColor,
|
||||
}}>
|
||||
{props.searchOutput &&
|
||||
<SearchBar onSubmitSearch={onSubmitSearch} />
|
||||
}
|
||||
<div style={{overflowY: "auto", flexGrow: 1, width: "100%", height: props.expand ? "100%": undefined, display: "flex", flexDirection: "column"}} ref={props.responseRef}>
|
||||
<ResponseDisplayComponent rawResponses={rawResponses} viewBrowserScript={props.viewBrowserScript}
|
||||
output={output} command_id={props.command_id}
|
||||
output={output} command_id={props.command_id} displayType={"accordion"}
|
||||
task={props.task} search={search.current} expand={props.expand}/>
|
||||
|
||||
</div>
|
||||
@@ -302,7 +318,7 @@ const NonInteractiveResponseDisplay = (props) => {
|
||||
</div>
|
||||
|
||||
}
|
||||
</React.Fragment>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export const ResponseDisplayConsole = (props) => {
|
||||
@@ -331,9 +347,12 @@ export const NonInteractiveResponseDisplayConsole = (props) => {
|
||||
setRawResponses(responseArray);
|
||||
} else {
|
||||
// we still have some room to view more, but only room for initialResponseStreamLimit - totalFetched.current
|
||||
const currentIDS = rawResponses.map( r => r.id);
|
||||
const newerResponses = data.data.response_stream.reduce( (prev, cur) => {
|
||||
if(currentIDS.includes(cur.id)){return prev}
|
||||
let prevIndex = prev.findIndex( (v,i,a) => v.id === cur.id);
|
||||
if(prevIndex >= 0){
|
||||
prev[prevIndex] = {...cur, response: b64DecodeUnicode(cur.response)};
|
||||
return prev;
|
||||
}
|
||||
return [...prev, {...cur, response: b64DecodeUnicode(cur.response)}]
|
||||
}, rawResponses);
|
||||
// sort them to make sure we're still in order
|
||||
@@ -350,20 +369,16 @@ export const NonInteractiveResponseDisplayConsole = (props) => {
|
||||
fetchPolicy: "network_only",
|
||||
onData: subscriptionDataCallback
|
||||
});
|
||||
const onSubmitSearch = React.useCallback( (newSearch) => {
|
||||
snackActions.warning("Search not supported for console view");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{props.searchOutput &&
|
||||
<SearchBar onSubmitSearch={onSubmitSearch} />
|
||||
}
|
||||
<div style={{overflowY: "auto", width: "100%", height: props.expand ? "100%": undefined}} ref={props.responseRef}>
|
||||
<div style={{overflowY: "auto", width: "100%", height: props.expand ? "100%" : undefined}}
|
||||
ref={props.responseRef}>
|
||||
<ResponseDisplayComponent rawResponses={rawResponses} viewBrowserScript={props.viewBrowserScript}
|
||||
output={output} command_id={props.command_id}
|
||||
output={output} command_id={props.command_id} displayType={"console"}
|
||||
task={props.task} search={""} expand={props.expand}/>
|
||||
</div>
|
||||
<div id={'scrolltotaskbottom' + props.task.id}></div>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
@@ -393,7 +408,7 @@ export const PaginationBar = ({selectAllOutput, totalCount, onSubmitPageChange,
|
||||
}, [totalCount, maxCount, search, selectAllOutput]);
|
||||
const pageCount = Math.max(1, Math.ceil(localTotalCount / pageSize));
|
||||
// don't bother people with pagination information if they haven't even started paginating
|
||||
if(pageCount < 2){
|
||||
if(pageCount < 2 || pageCount === Infinity || isNaN(pageCount)){
|
||||
return (<div id={'scrolltotaskbottom' + task.id}></div>)
|
||||
}
|
||||
return (
|
||||
@@ -429,27 +444,11 @@ export const SearchBar = ({onSubmitSearch}) => {
|
||||
);
|
||||
}
|
||||
|
||||
const ResponseDisplayComponent = ({rawResponses, viewBrowserScript, output, command_id, task, search, expand}) => {
|
||||
const ResponseDisplayComponent = ({rawResponses, viewBrowserScript, output, command_id, task, search, expand, displayType}) => {
|
||||
const [localViewBrowserScript, setViewBrowserScript] = React.useState(true);
|
||||
const [browserScriptData, setBrowserScriptData] = React.useState({});
|
||||
const [loadingBrowserScript, setLoadingBrowserScript] = React.useState(true);
|
||||
const useNewBrowserScriptTable = useMythicSetting({setting_name: "experiment-browserscripttable", default_value: "false"});
|
||||
const script = React.useRef();
|
||||
const me = useReactiveVar(meState);
|
||||
useEffect( () => {
|
||||
if(script.current !== undefined){
|
||||
try{
|
||||
const rawResponseData = rawResponses.map(c => c.response);
|
||||
let res = script.current(task, rawResponseData);
|
||||
setBrowserScriptData(filterOutput(res));
|
||||
}catch(error){
|
||||
setViewBrowserScript(false);
|
||||
setBrowserScriptData({});
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
}
|
||||
}, [rawResponses, task]);
|
||||
const script = React.useRef(undefined);
|
||||
const filterOutput = (scriptData) => {
|
||||
if(search === ""){
|
||||
return scriptData;
|
||||
@@ -461,67 +460,71 @@ const ResponseDisplayComponent = ({rawResponses, viewBrowserScript, output, comm
|
||||
copied["plaintext"] = "";
|
||||
}
|
||||
}
|
||||
if(useNewBrowserScriptTable){return copied}
|
||||
if(scriptData["table"] !== undefined){
|
||||
if(scriptData["table"].length > 0){
|
||||
const tableUpdates = scriptData.table.map( t => {
|
||||
const filteredRows = t.rows.filter( r => {
|
||||
copied["table"] = scriptData.table.map(t => {
|
||||
const filteredRows = t.rows.filter(r => {
|
||||
let foundMatch = false;
|
||||
for (const entry of Object.values(r)) {
|
||||
if(entry["plaintext"] !== undefined){
|
||||
if(String(entry["plaintext"]).toLowerCase().includes(search)){foundMatch = true;}
|
||||
if (entry["plaintext"] !== undefined) {
|
||||
if (String(entry["plaintext"]).toLowerCase().includes(search)) {
|
||||
foundMatch = true;
|
||||
}
|
||||
}
|
||||
if(entry["button"] !== undefined && entry["button"]["value"] !== undefined){
|
||||
if(JSON.stringify(entry["button"]["value"]).includes(search)){foundMatch = true;}
|
||||
if (entry["button"] !== undefined && entry["button"]["value"] !== undefined) {
|
||||
if (JSON.stringify(entry["button"]["value"]).includes(search)) {
|
||||
foundMatch = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return foundMatch;
|
||||
});
|
||||
return {...t, rows: filteredRows};
|
||||
});
|
||||
copied["table"] = tableUpdates;
|
||||
}
|
||||
}
|
||||
|
||||
return copied;
|
||||
}
|
||||
useEffect( () => {
|
||||
if(loadingBrowserScript){
|
||||
return;
|
||||
}
|
||||
if(script.current === undefined){
|
||||
setViewBrowserScript(false);
|
||||
}else{
|
||||
setViewBrowserScript(viewBrowserScript);
|
||||
if(viewBrowserScript){
|
||||
try{
|
||||
const rawResponseData = rawResponses.map(c => c.response);
|
||||
let scriptTaskData = JSON.parse(JSON.stringify(task));
|
||||
let res = script.current(scriptTaskData, rawResponseData);
|
||||
setBrowserScriptData(filterOutput(res));
|
||||
}catch(error){
|
||||
setViewBrowserScript(false);
|
||||
}
|
||||
|
||||
}
|
||||
return;
|
||||
}
|
||||
}, [viewBrowserScript]);
|
||||
if(viewBrowserScript){
|
||||
try{
|
||||
const rawResponseData = rawResponses.map(c => c.response);
|
||||
let res = script.current(task, rawResponseData);
|
||||
if(Object.keys(res).length === 0){
|
||||
// if we run the browser script and there's no data, just display plaintext
|
||||
setViewBrowserScript(false);
|
||||
return;
|
||||
}
|
||||
setViewBrowserScript(viewBrowserScript);
|
||||
setBrowserScriptData(filterOutput(res));
|
||||
}catch(error){
|
||||
if(rawResponses.length > 0){
|
||||
setViewBrowserScript(false);
|
||||
setBrowserScriptData({});
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setViewBrowserScript(false);
|
||||
}
|
||||
}, [rawResponses, task, loadingBrowserScript, viewBrowserScript]);
|
||||
const [fetchScripts] = useLazyQuery(taskScript, {
|
||||
fetchPolicy: "no-cache",
|
||||
onCompleted: (data) => {
|
||||
if(data.browserscript.length > 0){
|
||||
try{
|
||||
//let unb64script = b64DecodeUnicode(data.browserscript[0]["script"]);
|
||||
//script.current = Function('"use strict";return(' + unb64script + ')')();
|
||||
script.current = Function(`"use strict";return(${data.browserscript[0]["script"]})`)();
|
||||
setViewBrowserScript(true);
|
||||
//console.log(rawResponses);
|
||||
const rawResponseData = rawResponses.map(c => c.response);
|
||||
let scriptTaskData = JSON.parse(JSON.stringify(task));
|
||||
let res = script.current(scriptTaskData, rawResponseData);
|
||||
setBrowserScriptData(filterOutput(res));
|
||||
}catch(error){
|
||||
//snackActions.error(error.toString());
|
||||
script.current = undefined;
|
||||
console.log(error);
|
||||
setViewBrowserScript(false);
|
||||
setBrowserScriptData({});
|
||||
}
|
||||
}else{
|
||||
setViewBrowserScript(false);
|
||||
@@ -536,46 +539,71 @@ const ResponseDisplayComponent = ({rawResponses, viewBrowserScript, output, comm
|
||||
useEffect( () => {
|
||||
if(command_id !== undefined){
|
||||
setLoadingBrowserScript(true);
|
||||
fetchScripts({variables: {command_id: command_id, operator_id: me.user.user_id}});
|
||||
fetchScripts({variables: {command_id: command_id}});
|
||||
}
|
||||
}, [command_id, task.id]);
|
||||
if(loadingBrowserScript){
|
||||
return null
|
||||
}
|
||||
return (
|
||||
localViewBrowserScript && browserScriptData ? (
|
||||
<React.Fragment>
|
||||
{browserScriptData?.screenshot?.map( (scr, index) => (
|
||||
<ResponseDisplayScreenshot key={"screenshot" + index + 'fortask' + task.id} task={task} {...scr} />
|
||||
)) || null
|
||||
}
|
||||
{browserScriptData?.plaintext !== undefined &&
|
||||
<ResponseDisplayPlaintext plaintext={browserScriptData["plaintext"]} task={task} expand={expand} />
|
||||
}
|
||||
{browserScriptData?.table?.map( (table, index) => {
|
||||
if(useNewBrowserScriptTable){
|
||||
return <ResponseDisplayMaterialReactTable callback_id={task.callback_id} task={task} expand={expand} table={table} key={"tablefortask" + task.id + "table" + index} />
|
||||
}
|
||||
return <ResponseDisplayTable callback_id={task.callback_id} task={task} expand={expand} table={table} key={"tablefortask" + task.id + "table" + index} />
|
||||
}) || null
|
||||
}
|
||||
{browserScriptData?.download?.map( (dl, index) => (
|
||||
<ResponseDisplayDownload download={dl} task={task} key={"download" + index + "fortask" + task.id} />
|
||||
)) || null
|
||||
}
|
||||
{browserScriptData?.search?.map( (s, index) => (
|
||||
<ResponseDisplaySearch search={s} task={task} key={"searchlink" + index + "fortask" + task.id} />
|
||||
)) || null
|
||||
}
|
||||
{browserScriptData?.media?.map( (s, index) => (
|
||||
<ResponseDisplayMedia key={"searchmedia" + index + "fortask" + task.id} task={task} media={s} expand={expand} />
|
||||
)) || null}
|
||||
{browserScriptData?.graph !== undefined &&
|
||||
<ResponseDisplayGraph graph={browserScriptData.graph} task={task} expand={expand} />
|
||||
}
|
||||
</React.Fragment>
|
||||
localViewBrowserScript ? (
|
||||
<ResponseDisplayBrowserScriptComponent expand={expand} displayType={displayType}
|
||||
task={task} browserScriptData={browserScriptData}
|
||||
output={output}/>
|
||||
) : (
|
||||
<ResponseDisplayPlaintext plaintext={output} task={task} expand={expand}/>
|
||||
<ResponseDisplayPlaintext plaintext={output} task={task} expand={expand} displayType={displayType}/>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export function ResponseDisplayBrowserScriptComponent({output, browserScriptData, task, expand, displayType}) {
|
||||
return (
|
||||
<>
|
||||
{Object.keys(browserScriptData).length > 0 ? (
|
||||
<>
|
||||
{browserScriptData?.screenshot?.map( (scr, index) => (
|
||||
<ResponseDisplayScreenshot key={"screenshot" + index + 'fortask' + task.id} task={task} {...scr}
|
||||
displayType={displayType} expand={expand} />
|
||||
))
|
||||
}
|
||||
{browserScriptData?.plaintext !== undefined &&
|
||||
<ResponseDisplayPlaintext plaintext={browserScriptData["plaintext"]} task={task}
|
||||
expand={expand} displayType={displayType} />
|
||||
}
|
||||
{browserScriptData?.table?.map( (table, index) => (
|
||||
<ResponseDisplayTable callback_id={task.callback_id} task={task} expand={expand}
|
||||
table={table} key={"tablefortask" + task.id + "table" + index}
|
||||
displayType={displayType}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{browserScriptData?.download?.map( (dl, index) => (
|
||||
<ResponseDisplayDownload download={dl} task={task} displayType={displayType}
|
||||
key={"download" + index + "fortask" + task.id} />
|
||||
))
|
||||
}
|
||||
{browserScriptData?.search?.map( (s, index) => (
|
||||
<ResponseDisplaySearch search={s} task={task} displayType={displayType}
|
||||
key={"searchlink" + index + "fortask" + task.id} />
|
||||
))
|
||||
}
|
||||
{browserScriptData?.media?.map( (s, index) => (
|
||||
<ResponseDisplayMedia key={"searchmedia" + index + "fortask" + task.id}
|
||||
displayType={displayType}
|
||||
task={task} media={s} expand={expand} />
|
||||
))}
|
||||
{browserScriptData?.graph !== undefined &&
|
||||
<ResponseDisplayGraph graph={browserScriptData.graph} task={task}
|
||||
expand={expand} displayType={displayType} />
|
||||
}
|
||||
{browserScriptData.tabs && browserScriptData.tabs.length > 0 &&
|
||||
<ResponseDisplayTabs task={task} expand={expand} displayType={displayType} tabs={browserScriptData.tabs} output={output} />
|
||||
}
|
||||
</>
|
||||
) : (
|
||||
<ResponseDisplayPlaintext plaintext={output} task={task} expand={expand} displayType={displayType}/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { gql, useMutation, useSubscription } from '@apollo/client';
|
||||
import {b64DecodeUnicode} from "./ResponseDisplay";
|
||||
import {SearchBar} from './ResponseDisplay';
|
||||
import Pagination from '@mui/material/Pagination';
|
||||
import {Typography, CircularProgress, Select, IconButton} from '@mui/material';
|
||||
import {Typography, CircularProgress, Select, IconButton, Backdrop} from '@mui/material';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import Input from '@mui/material/Input';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
@@ -18,6 +18,10 @@ import KeyboardReturnIcon from '@mui/icons-material/KeyboardReturn';
|
||||
import InputLabel from "@mui/material/InputLabel";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import WrapTextIcon from '@mui/icons-material/WrapText';
|
||||
import { useReactiveVar } from '@apollo/client';
|
||||
import { meState } from '../../../cache';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import HeightIcon from '@mui/icons-material/Height';
|
||||
|
||||
const getInteractiveTaskingQuery = gql`
|
||||
${taskingDataFragment}
|
||||
@@ -37,10 +41,13 @@ subscription subResponsesQuery($task_id: Int!) {
|
||||
}
|
||||
}`;
|
||||
const getTaskingStatus = (task) => {
|
||||
if(task.status === "completed"){
|
||||
if(task.status === "completed" || task.status === "success"){
|
||||
return <CheckCircleOutlineIcon color={"success"} fontSize={"1rem"} style={{marginRight: "2px"}} />
|
||||
} else if(task.status === "submitted"){
|
||||
return <CircularProgress size={"1rem"} />
|
||||
} else {
|
||||
console.log(task.status);
|
||||
return null
|
||||
}
|
||||
}
|
||||
const getClassnames = (entry) => {
|
||||
@@ -73,57 +80,89 @@ const getClassnames = (entry) => {
|
||||
//console.log(entry);
|
||||
return classnames.join(" ");
|
||||
}
|
||||
const GetOutputFormat = ({data, useASNIColor, messagesEndRef, showTaskStatus, wrapText}) => {
|
||||
const handleTerminalCodes = (response) => {
|
||||
let output = response.replaceAll("[?2004h", "");
|
||||
output = output.replaceAll("[?2004l", "");
|
||||
let indexOfTitleSeq = output.indexOf("]0");
|
||||
if(indexOfTitleSeq >= 0){
|
||||
let endIndexOfTitleSeq = output.indexOf("]");
|
||||
if(endIndexOfTitleSeq >= 0 && endIndexOfTitleSeq > indexOfTitleSeq){
|
||||
output = output.substring(0, indexOfTitleSeq + 2) + output.substring(endIndexOfTitleSeq);
|
||||
}
|
||||
}
|
||||
let indexOfClearLeft = output.indexOf("[J");
|
||||
if(indexOfClearLeft >= 0){
|
||||
let indexOfLastNewLine = 0;
|
||||
for(let i = indexOfClearLeft; i >= 0; i--){
|
||||
if(output[i] === "\n"){
|
||||
indexOfLastNewLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
output = output.substring(0, indexOfLastNewLine+1) + output.substring(indexOfClearLeft + 2);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
export const GetOutputFormatAll = ({data, myTask, taskID, useASNIColor, messagesEndRef, showTaskStatus, wrapText, search}) => {
|
||||
const [dataElement, setDataElement] = React.useState(null);
|
||||
React.useEffect( () => {
|
||||
|
||||
if(data.response) {
|
||||
// we're looking at response output
|
||||
if(data.is_error){
|
||||
setDataElement(<pre style={{display: "inline",backgroundColor: "#311717", margin: "0 0 0 0",
|
||||
wordBreak: wrapText ? "break-all" : "",
|
||||
whiteSpace: wrapText ? "pre-wrap" : ""}} key={data.timestamp + data.id}>
|
||||
{data.response}
|
||||
</pre>)
|
||||
} else {
|
||||
if(useASNIColor){
|
||||
let ansiJSON = Anser.ansiToJson(data.response, { use_classes: true });
|
||||
//console.log(ansiJSON)
|
||||
setDataElement(
|
||||
ansiJSON.map( (a, i) => (
|
||||
<pre style={{display: "inline", margin: "0 0 0 0",
|
||||
wordBreak: wrapText ? "break-all" : "",
|
||||
whiteSpace: wrapText ? "pre-wrap" : "",
|
||||
}} className={getClassnames(a)} key={data.id + data.timestamp + i}>{a.content}</pre>
|
||||
))
|
||||
)
|
||||
} else {
|
||||
setDataElement(<pre style={{display: "inline", margin: "0 0 0 0",
|
||||
const elements = data.map( d => {
|
||||
if(d.response !== undefined) {
|
||||
// we're looking at response output
|
||||
if(d.is_error){
|
||||
return (<pre id={"response" + d.timestamp + d.id} style={{display: "inline",backgroundColor: "#311717", color: "white", margin: "0 0 0 0",
|
||||
wordBreak: wrapText ? "break-all" : "",
|
||||
whiteSpace: wrapText ? "pre-wrap" : "",
|
||||
}} key={data.timestamp + data.id}>{data.response}</pre>)
|
||||
}
|
||||
whiteSpace: wrapText ? "pre-wrap" : ""}} key={d.timestamp + d.id}>
|
||||
{d.response}
|
||||
</pre>)
|
||||
} else {
|
||||
if(useASNIColor){
|
||||
let removedTerminalCodes = handleTerminalCodes(d.response);
|
||||
let ansiJSON = Anser.ansiToJson(removedTerminalCodes, { use_classes: true });
|
||||
//console.log(ansiJSON)
|
||||
return (
|
||||
ansiJSON.map( (a, i) => (
|
||||
<pre id={"response" + d.timestamp + d.id} style={{display: "inline", margin: "0 0 0 0",
|
||||
wordBreak: wrapText ? "break-all" : "",
|
||||
whiteSpace: wrapText ? "pre-wrap" : "",
|
||||
}} className={getClassnames(a)} key={d.id + d.timestamp + i}>{a.content}</pre>
|
||||
))
|
||||
)
|
||||
} else {
|
||||
return (<pre id={"response" + d.timestamp + d.id} style={{display: "inline", margin: "0 0 0 0",
|
||||
wordBreak: wrapText ? "break-all" : "",
|
||||
whiteSpace: wrapText ? "pre-wrap" : "",
|
||||
}} key={d.timestamp + d.id}>{d.response}</pre>)
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
// we're looking at tasking
|
||||
setDataElement(
|
||||
<pre key={data.timestamp + data.id} style={{display: "inline",margin: "0 0 0 0",
|
||||
wordBreak: wrapText ? "break-all" : "", whiteSpace: "pre-wrap"}}>
|
||||
{showTaskStatus && getTaskingStatus(data)}
|
||||
{data.original_params}
|
||||
}
|
||||
} else {
|
||||
// we're looking at tasking
|
||||
return(
|
||||
<pre id={"task" + d.timestamp + d.id} key={d.timestamp + d.id} style={{display: "inline",margin: "0 0 0 0",
|
||||
wordBreak: wrapText ? "break-all" : "", whiteSpace: "pre-wrap"}}>
|
||||
{showTaskStatus && getTaskingStatus(d)}
|
||||
{d.original_params}
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
}, [data.timestamp, useASNIColor, showTaskStatus, wrapText]);
|
||||
/*
|
||||
React.useEffect( () => {
|
||||
if(messagesEndRef.current){
|
||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
||||
)
|
||||
}
|
||||
})
|
||||
setDataElement(elements);
|
||||
}, [data, useASNIColor, showTaskStatus, wrapText]);
|
||||
React.useLayoutEffect( () => {
|
||||
if(myTask){
|
||||
let el = document.getElementById(`ptytask${taskID}`);
|
||||
if(el && el.scrollHeight - el.scrollTop - el.clientHeight < 500){
|
||||
if(!search){
|
||||
messagesEndRef?.current?.scrollIntoView({ behavior: "auto", block: "nearest" });
|
||||
//console.log("scrolling");
|
||||
}
|
||||
} else {
|
||||
// console.log("not scrolled down enough")
|
||||
}
|
||||
}
|
||||
}, [dataElement]);
|
||||
|
||||
*/
|
||||
return (
|
||||
dataElement
|
||||
)
|
||||
@@ -134,6 +173,7 @@ const InteractiveMessageTypes = [
|
||||
{"name": "None", "value": -1, "text": "None"},
|
||||
{"name": "Tab", "value": 13, "text": "^I"},
|
||||
{"name": "Backspace", "value": 12, "text": "^H"},
|
||||
{"name": "Exit", "value": 3, "text": "exit"},
|
||||
{"name": "Escape", "value": 4, "text": "^["},
|
||||
{"name": "Ctrl+A", "value": 5, "text": "^A"},
|
||||
{"name": "Ctrl+B", "value": 6, "text": "^B"},
|
||||
@@ -161,6 +201,10 @@ const EnterOptions = [
|
||||
{"value": "\r\n", "name": "CRLF"}
|
||||
];
|
||||
export const ResponseDisplayInteractive = (props) =>{
|
||||
const me = useReactiveVar(meState);
|
||||
const theme = useTheme();
|
||||
const [backdropOpen, setBackdropOpen] = React.useState(false);
|
||||
const [scrollToBottom, setScrollToBottom] = React.useState(true);
|
||||
const pageSize = React.useRef(100);
|
||||
const highestFetched = React.useRef(0);
|
||||
const [taskData, setTaskData] = React.useState([]);
|
||||
@@ -175,9 +219,9 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
const [useASNIColor, setUseANSIColor] = React.useState(true);
|
||||
const [showTaskStatus, setShowTaskStatus] = React.useState(true);
|
||||
const [wrapText, setWrapText] = React.useState(true);
|
||||
useSubscription(getInteractiveTaskingQuery, {
|
||||
const [autoScroll, setAutoScroll] = React.useState(true);
|
||||
const {loading: loadingTasks} = useSubscription(getInteractiveTaskingQuery, {
|
||||
variables: {parent_task_id: props.task.id},
|
||||
shouldResubscribe: true,
|
||||
onError: data => {
|
||||
console.error(data)
|
||||
},
|
||||
@@ -198,15 +242,19 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
}, [...taskData]);
|
||||
setTaskData(newTaskData);
|
||||
}
|
||||
if(backdropOpen){
|
||||
setBackdropOpen(false);
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
})
|
||||
const subscriptionDataCallback = React.useCallback( ({data}) => {
|
||||
// we still have some room to view more, but only room for fetchLimit - totalFetched.current
|
||||
if(props.task.id !== taskIDRef.current){
|
||||
const newResponses = data.data.response_stream;
|
||||
const newerResponses = newResponses.map( (r) => { return {...r, response: b64DecodeUnicode(r.response)}});
|
||||
newerResponses.sort( (a,b) => a.id > b.id ? 1 : -1);
|
||||
let rawResponseArray = [...rawResponses];
|
||||
let rawResponseArray = [];
|
||||
let highestFetchedId = 0;
|
||||
for(let i = 0; i < newerResponses.length; i++){
|
||||
rawResponseArray.push(newerResponses[i]);
|
||||
@@ -216,23 +264,27 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
highestFetched.current = highestFetchedId;
|
||||
taskIDRef.current = props.task.id;
|
||||
} else {
|
||||
const newResponses = data.data.response_stream.filter( r => r.id > highestFetched.current);
|
||||
const newerResponses = newResponses.map( (r) => { return {...r, response: b64DecodeUnicode(r.response)}});
|
||||
newerResponses.sort( (a,b) => a.id > b.id ? 1 : -1);
|
||||
const newResponses = data.data.response_stream.filter(r => r.id > highestFetched.current);
|
||||
const newerResponses = newResponses.map((r) => {
|
||||
return {...r, response: b64DecodeUnicode(r.response)}
|
||||
});
|
||||
newerResponses.sort((a, b) => a.id > b.id ? 1 : -1);
|
||||
let rawResponseArray = [...rawResponses];
|
||||
let highestFetchedId = highestFetched.current;
|
||||
for(let i = 0; i < newerResponses.length; i++){
|
||||
for (let i = 0; i < newerResponses.length; i++) {
|
||||
rawResponseArray.push(newerResponses[i]);
|
||||
highestFetchedId = newerResponses[i]["id"];
|
||||
}
|
||||
setRawResponses(rawResponseArray);
|
||||
highestFetched.current = highestFetchedId;
|
||||
}
|
||||
|
||||
}, [highestFetched.current, rawResponses, props.task.id]);
|
||||
if(backdropOpen){
|
||||
setBackdropOpen(false);
|
||||
}
|
||||
}, [highestFetched.current, rawResponses, props.task.id, backdropOpen, taskIDRef.current, autoScroll]);
|
||||
useSubscription(subResponsesQuery, {
|
||||
variables: {task_id: props.task.id},
|
||||
fetchPolicy: "network_only",
|
||||
fetchPolicy: "no-cache",
|
||||
onData: subscriptionDataCallback
|
||||
});
|
||||
const onSubmitPageChange = (currentPage) => {
|
||||
@@ -240,20 +292,21 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
if(search === undefined || search === ""){
|
||||
let allData = [...rawResponses, ...taskData];
|
||||
allData.sort( (a,b) => {
|
||||
let aDate = new Date(a.timestamp);
|
||||
let bDate = new Date(b.timestamp);
|
||||
let aDate = new Date(a?.status_timestamp_preprocessing || a.timestamp);
|
||||
let bDate = new Date(b?.status_timestamp_preprocessing || b.timestamp);
|
||||
return aDate < bDate ? -1 : bDate < aDate ? 1 : 0;
|
||||
})
|
||||
if(page.current === currentPage){
|
||||
const pageCount = Math.max(1, Math.ceil(allData.length / pageSize.current));
|
||||
if(page.current === pageCount -1){
|
||||
console.log("updating pageSize");
|
||||
// we just streamed more data and we're on the latest page, increase pageSize
|
||||
//pageSize.current += Math.abs(allData.length - totalCount);
|
||||
currentPage += 1;
|
||||
}
|
||||
}
|
||||
setAllOutput(allData.slice((currentPage-1)*pageSize.current, currentPage*pageSize.current));
|
||||
if(props.selectAllOutput){
|
||||
setAllOutput(allData);
|
||||
} else {
|
||||
setAllOutput(allData.slice((currentPage-1)*pageSize.current, currentPage*pageSize.current));
|
||||
}
|
||||
setTotalCount(allData.length);
|
||||
}else{
|
||||
let allData = [...rawResponses, ...taskData];
|
||||
@@ -265,8 +318,8 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
}
|
||||
})
|
||||
allData.sort( (a,b) => {
|
||||
let aDate = new Date(a.timestamp);
|
||||
let bDate = new Date(b.timestamp);
|
||||
let aDate = new Date(a?.status_timestamp_preprocessing || a.timestamp);
|
||||
let bDate = new Date(b?.status_timestamp_preprocessing || b.timestamp);
|
||||
return aDate < bDate ? -1 : bDate < aDate ? 1 : 0;
|
||||
});
|
||||
|
||||
@@ -286,6 +339,9 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
React.useEffect( () => {
|
||||
onSubmitPageChange(1);
|
||||
}, [search]);
|
||||
React.useEffect( () => {
|
||||
onSubmitPageChange(1);
|
||||
}, [props.selectAllOutput]);
|
||||
const onSubmitSearch = React.useCallback( (newSearch) => {
|
||||
setSearch(newSearch);
|
||||
}, []);
|
||||
@@ -312,50 +368,93 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
const toggleWrapText = () => {
|
||||
setWrapText(!wrapText);
|
||||
}
|
||||
const scrollContent = (node, isAppearing) => {
|
||||
// only auto-scroll if you issued the task
|
||||
document.getElementById(`scrolltotaskbottom${props.task.id}`)?.scrollIntoView({
|
||||
//behavior: "smooth",
|
||||
block: "end",
|
||||
inline: "nearest"
|
||||
})
|
||||
const toggleAutoScroll = () => {
|
||||
setAutoScroll(!autoScroll);
|
||||
}
|
||||
React.useLayoutEffect( () => {
|
||||
scrollContent()
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if(autoScroll){
|
||||
messagesEndRef?.current?.scrollIntoView({ behavior: "auto", block: "nearest" });
|
||||
}
|
||||
}, [props.responseRef?.current?.scrollHeight, autoScroll]);
|
||||
React.useEffect( () => {
|
||||
if(loadingTasks){
|
||||
setTaskData([]);
|
||||
setBackdropOpen(true);
|
||||
}else{
|
||||
setBackdropOpen(false);
|
||||
}
|
||||
}, [loadingTasks]);
|
||||
setTimeout(() => {
|
||||
// close the backdrop after 2 seconds in case there's no data to fetch
|
||||
setBackdropOpen(false);
|
||||
}, 2000);
|
||||
return (
|
||||
|
||||
<div style={{ display: "flex", overflowY: "auto",
|
||||
position: "relative", height: props.expand ? "100%": undefined, maxHeight: props.expand ? "100%": "500px",
|
||||
flexDirection: "column"}}>
|
||||
{props.searchOutput &&
|
||||
<SearchBar onSubmitSearch={onSubmitSearch} />
|
||||
}
|
||||
<div style={{overflowY: "auto", width: "100%", flexGrow: 1}} ref={props.responseRef}>
|
||||
{alloutput.map( (e, index) => (
|
||||
<GetOutputFormat key={"getoutput" + index} data={e} useASNIColor={useASNIColor} messagesEndRef={messagesEndRef}
|
||||
showTaskStatus={showTaskStatus} wrapText={wrapText} />
|
||||
))}
|
||||
<div ref={messagesEndRef}/>
|
||||
</div>
|
||||
<div style={{
|
||||
display: "flex", overflowY: "auto",
|
||||
position: "relative",
|
||||
height: props.expand ? "100%" : undefined,
|
||||
maxHeight: props.expand ? "100%" : "500px",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
backgroundColor: theme.outputBackgroundColor + (theme.palette.mode === 'dark' ? "B0" : "E0"),
|
||||
color: theme.outputTextColor,
|
||||
}}>
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute",}} invisible={false}>
|
||||
<div style={{
|
||||
borderRadius: "4px",
|
||||
border: "1px solid black",
|
||||
padding: "5px",
|
||||
backgroundColor: "rgba(37,37,37,0.92)", color: "white",
|
||||
alignItems: "center",
|
||||
display: "flex", flexDirection: "column"}}>
|
||||
<CircularProgress color="inherit" />
|
||||
<Typography variant={"h5"}>
|
||||
Fetching Interactive Task Data....
|
||||
</Typography>
|
||||
</div>
|
||||
</Backdrop>
|
||||
{props.searchOutput &&
|
||||
<SearchBar onSubmitSearch={onSubmitSearch}/>
|
||||
}
|
||||
<div style={{overflowY: "auto", width: "100%", marginBottom: "5px", height: props.expand ? "100%": undefined,
|
||||
flexGrow: 1, paddingLeft: "10px", minHeight: "50px"}} ref={props.responseRef}
|
||||
id={`ptytask${props.task.id}`}>
|
||||
<GetOutputFormatAll data={alloutput}
|
||||
myTask={props.task.operator.username === (me?.user?.username || "")}
|
||||
taskID={props.task.id}
|
||||
useASNIColor={useASNIColor}
|
||||
messagesEndRef={messagesEndRef}
|
||||
showTaskStatus={showTaskStatus}
|
||||
search={props.searchOutput ? search : undefined}
|
||||
wrapText={wrapText}/>
|
||||
<div ref={messagesEndRef}/>
|
||||
</div>
|
||||
{!props.task?.is_interactive_task &&
|
||||
<div style={{width: "100%", display: "inline-flex", alignItems: "flex-end"}}>
|
||||
<InteractiveTaskingBar task={props.task} taskData={taskData}
|
||||
useASNIColor={useASNIColor} toggleANSIColor={toggleANSIColor}
|
||||
showTaskStatus={showTaskStatus} toggleShowTaskStatus={toggleShowTaskStatus}
|
||||
wrapText={wrapText} toggleWrapText={toggleWrapText}
|
||||
autoScroll={autoScroll} toggleAutoScroll={toggleAutoScroll}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div style={{width: "100%", display: "inline-flex", alignItems: "flex-end"}} >
|
||||
<InteractiveTaskingBar task={props.task} taskData={taskData}
|
||||
useASNIColor={useASNIColor} toggleANSIColor={toggleANSIColor}
|
||||
showTaskStatus={showTaskStatus} toggleShowTaskStatus={toggleShowTaskStatus}
|
||||
wrapText={wrapText} toggleWrapText={toggleWrapText}
|
||||
/>
|
||||
</div>
|
||||
<InteractivePaginationBar totalCount={totalCount} currentPage={page.current}
|
||||
onSubmitPageChange={onSubmitPageChange} expand={props.expand}
|
||||
pageSize={pageSize.current} />
|
||||
</div>
|
||||
<InteractivePaginationBar totalCount={totalCount} currentPage={page.current}
|
||||
onSubmitPageChange={onSubmitPageChange} expand={props.expand}
|
||||
pageSize={pageSize.current} selectAllOutput={props.selectAllOutput}/>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
const InteractiveTaskingBar = ({task, taskData, useASNIColor, toggleANSIColor,
|
||||
showTaskStatus, toggleShowTaskStatus, wrapText, toggleWrapText}) => {
|
||||
const InteractiveTaskingBar = ({
|
||||
task, taskData, useASNIColor, toggleANSIColor,
|
||||
showTaskStatus, toggleShowTaskStatus, wrapText, toggleWrapText,
|
||||
autoScroll, toggleAutoScroll
|
||||
}) => {
|
||||
const [inputText, setInputText] = React.useState("");
|
||||
const theme = useTheme();
|
||||
const [createTask] = useMutation(createTaskingMutation, {
|
||||
update: (cache, {data}) => {
|
||||
if(data.createTask.status === "error"){
|
||||
@@ -369,65 +468,74 @@ const InteractiveTaskingBar = ({task, taskData, useASNIColor, toggleANSIColor,
|
||||
});
|
||||
const [taskOptionsIndex, setTaskOptionsIndex] = React.useState(-1);
|
||||
const [taskOptions, setTaskOptions] = React.useState([]);
|
||||
const [selectedEnterOption, setSelectedEnterOption] = React.useState(EnterOptions[1]);
|
||||
const [selectedControlOption, setSelectedControlOption] = React.useState(InteractiveMessageTypes[0]);
|
||||
const [selectedEnterOption, setSelectedEnterOption] = React.useState(1);
|
||||
const [selectedControlOption, setSelectedControlOption] = React.useState(0);
|
||||
React.useEffect( () => {
|
||||
const newTaskOptions = taskData.filter( t => t.display_params.length > 1 && (t.interactive_task_type === 0 || t.interactive_task_type === 8));
|
||||
newTaskOptions.sort( (a,b) => a.id > b.id ? -1 : 1);
|
||||
setTaskOptions(newTaskOptions);
|
||||
}, [taskData]);
|
||||
const onInputChange = (name, value, error, event) => {
|
||||
if(event.key === "ArrowUp"){
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if(taskOptions.length === 0){
|
||||
snackActions.warning("No previous tasks")
|
||||
return;
|
||||
}
|
||||
let newIndex = (taskOptionsIndex + 1);
|
||||
if(newIndex > taskOptions.length -1){
|
||||
newIndex = taskOptions.length -1;
|
||||
}
|
||||
setTaskOptionsIndex(newIndex);
|
||||
setInputText(taskOptions[newIndex].display_params.trim());
|
||||
}else if(event.key === "ArrowDown"){
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if(taskData.length === 0){
|
||||
snackActions.warning("No next tasks")
|
||||
return;
|
||||
}
|
||||
let newIndex = (taskOptionsIndex -1);
|
||||
if(newIndex < 0){
|
||||
setTaskOptionsIndex(-1);
|
||||
setInputText("");
|
||||
} else {
|
||||
if(event !== null && event !== undefined){
|
||||
if(event.key === "ArrowUp"){
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if(taskOptions.length === 0){
|
||||
snackActions.warning("No previous tasks")
|
||||
return;
|
||||
}
|
||||
let newIndex = (taskOptionsIndex + 1);
|
||||
if(newIndex > taskOptions.length -1){
|
||||
newIndex = taskOptions.length -1;
|
||||
}
|
||||
setTaskOptionsIndex(newIndex);
|
||||
setInputText(taskOptions[newIndex].display_params.trim());
|
||||
}
|
||||
}else if(event.key === "ArrowDown"){
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if(taskData.length === 0){
|
||||
snackActions.warning("No next tasks")
|
||||
return;
|
||||
}
|
||||
let newIndex = (taskOptionsIndex -1);
|
||||
if(newIndex < 0){
|
||||
setTaskOptionsIndex(-1);
|
||||
setInputText("");
|
||||
} else {
|
||||
setTaskOptionsIndex(newIndex);
|
||||
setInputText(taskOptions[newIndex].display_params.trim());
|
||||
}
|
||||
|
||||
}else{
|
||||
}else{
|
||||
setInputText(value);
|
||||
}
|
||||
} else {
|
||||
setInputText(value);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
const submitTask = (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if(event.shiftKey){
|
||||
setInputText(inputText + selectedEnterOption.value);
|
||||
setInputText(inputText + EnterOptions[selectedEnterOption].value);
|
||||
return;
|
||||
}
|
||||
if(selectedControlOption.value > 0){
|
||||
let ctrlSequence = selectedControlOption.text;
|
||||
let enterOption = selectedEnterOption.value;
|
||||
if(event.metaKey || event.ctrlKey){
|
||||
setInputText(inputText + EnterOptions[selectedEnterOption].value);
|
||||
return;
|
||||
}
|
||||
if(InteractiveMessageTypes[selectedControlOption].value > 0){
|
||||
let ctrlSequence = InteractiveMessageTypes[selectedControlOption].text;
|
||||
let enterOption = EnterOptions[selectedEnterOption].value;
|
||||
let originalParams = inputText + ctrlSequence + enterOption;
|
||||
// if we're looking at a tab, never send enter along with it
|
||||
if (selectedControlOption.value === 8){
|
||||
if (InteractiveMessageTypes[selectedControlOption].value === 8){
|
||||
originalParams = inputText + ctrlSequence;
|
||||
enterOption = "";
|
||||
// if we're looking at escape, never send enter along with it
|
||||
} else if(selectedControlOption.value === 4){
|
||||
} else if(InteractiveMessageTypes[selectedControlOption].value === 4){
|
||||
originalParams = ctrlSequence + inputText;
|
||||
enterOption = "";
|
||||
}
|
||||
@@ -440,16 +548,16 @@ const InteractiveTaskingBar = ({task, taskData, useASNIColor, toggleANSIColor,
|
||||
parameter_group_name: "default",
|
||||
parent_task_id: task.id,
|
||||
is_interactive_task: true,
|
||||
interactive_task_type: selectedControlOption.value,
|
||||
interactive_task_type: InteractiveMessageTypes[selectedControlOption].value,
|
||||
}})
|
||||
}else {
|
||||
// no control option selected, just send data along as input
|
||||
createTask({variables: {
|
||||
callback_id: task.callback.display_id,
|
||||
command: task.command.cmd,
|
||||
params: inputText + selectedEnterOption.value,
|
||||
params: inputText + EnterOptions[selectedEnterOption].value,
|
||||
tasking_location: "command_line",
|
||||
original_params: inputText + selectedEnterOption.value,
|
||||
original_params: inputText + EnterOptions[selectedEnterOption].value,
|
||||
parameter_group_name: "default",
|
||||
parent_task_id: task.id,
|
||||
is_interactive_task: true,
|
||||
@@ -457,7 +565,8 @@ const InteractiveTaskingBar = ({task, taskData, useASNIColor, toggleANSIColor,
|
||||
}})
|
||||
}
|
||||
setInputText("");
|
||||
setSelectedControlOption(InteractiveMessageTypes[0]);
|
||||
setSelectedControlOption(0);
|
||||
setTaskOptionsIndex(-1);
|
||||
}
|
||||
const onChangeSelect = (event) => {
|
||||
event.stopPropagation();
|
||||
@@ -470,86 +579,102 @@ const InteractiveTaskingBar = ({task, taskData, useASNIColor, toggleANSIColor,
|
||||
setSelectedEnterOption(event.target.value);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<FormControl style={{width: "10rem", marginTop: "2px"}} >
|
||||
<InputLabel id="control-label" style={{}}>Controls</InputLabel>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-end",
|
||||
//backgroundColor: theme.outputBackgroundColor + (theme.palette.mode === 'dark' ? "F0" : "50"),
|
||||
color: theme.outputTextColor,
|
||||
paddingTop: "5px",
|
||||
border: "1px solid grey",
|
||||
borderRadius: "4px",
|
||||
width: "100%"}}>
|
||||
<FormControl style={{width: "10rem", marginTop: "2px", color: theme.outputTextColor}} >
|
||||
<InputLabel id="control-label" style={{color: theme.outputTextColor}}>Controls</InputLabel>
|
||||
<Select
|
||||
labelId="control-label"
|
||||
id="control-select"
|
||||
value={selectedControlOption}
|
||||
onChange={onChangeSelect}
|
||||
input={<Input style={{margin: 0}} />}
|
||||
sx={{
|
||||
// Target the icon element directly within the Select
|
||||
'.MuiSelect-icon': {
|
||||
color: theme.outputTextColor, // Set your desired color
|
||||
},
|
||||
}}
|
||||
input={<Input style={{margin: 0, color: theme.outputTextColor}} />}
|
||||
>
|
||||
{InteractiveMessageTypes.map( (opt) => (
|
||||
<MenuItem value={opt} key={opt.name}>{opt.name}</MenuItem>
|
||||
{InteractiveMessageTypes.map( (opt,index) => (
|
||||
<MenuItem value={index} key={opt.name}>{opt.name}</MenuItem>
|
||||
) )}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<MythicTextField autoFocus={true} maxRows={5} multiline={true} onChange={onInputChange} onEnter={submitTask}
|
||||
value={inputText} variant={"standard"} placeholder={">_ type here..."} inline={true}
|
||||
marginBottom={"0px"} InputProps={{style: { width: "100%"}}}/>
|
||||
<FormControl style={{width: "6rem"}} >
|
||||
debounceDelay={50}
|
||||
marginBottom={"0px"} InputProps={{style: { width: "100%", color: theme.outputTextColor}}}/>
|
||||
<FormControl style={{width: "7rem", color: theme.outputTextColor}} >
|
||||
<Select
|
||||
labelId="linefeed-label"
|
||||
id="linefeed-control"
|
||||
value={selectedEnterOption}
|
||||
autoWidth
|
||||
sx={{
|
||||
// Target the icon element directly within the Select
|
||||
'.MuiSelect-icon': {
|
||||
color: theme.outputTextColor, // Set your desired color
|
||||
},
|
||||
}}
|
||||
onChange={onChangeSelectEnterOption}
|
||||
input={<Input />}
|
||||
input={<Input style={{color: theme.outputTextColor}} />}
|
||||
IconComponent={KeyboardReturnIcon}
|
||||
>
|
||||
{EnterOptions.map( (opt) => (
|
||||
<MenuItem value={opt} key={opt.name}>{opt.name}</MenuItem>
|
||||
{EnterOptions.map( (opt,index) => (
|
||||
<MenuItem value={index} key={opt.name}>{opt.name}</MenuItem>
|
||||
) )}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<MythicStyledTooltip title={useASNIColor ? "Disable ANSI Color" : "Enable ANSI Color"} >
|
||||
<IconButton onClick={toggleANSIColor} style={{paddingLeft: 0, paddingRight: 0}}>
|
||||
<IconButton onClick={toggleANSIColor} style={{paddingLeft: 0, paddingRight: 0,}} disableRipple={true} disableFocusRipple={true}>
|
||||
<PaletteIcon color={useASNIColor ? "success" : "secondary"}
|
||||
style={{cursor: "pointer"}}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={showTaskStatus ? "Hide Task Status" : "Show Task Status"} >
|
||||
<IconButton onClick={toggleShowTaskStatus} style={{paddingLeft: 0, paddingRight: 0}}>
|
||||
<IconButton onClick={toggleShowTaskStatus} style={{paddingLeft: 0, paddingRight: 0}} disableRipple={true} disableFocusRipple={true}>
|
||||
<CheckCircleOutlineIcon color={showTaskStatus ? "success" : "secondary"}
|
||||
style={{cursor: "pointer",}}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={wrapText ? "Unwrap Text" : "Wrap Text"} >
|
||||
<IconButton onClick={toggleWrapText} style={{paddingLeft: 0, paddingRight: 0}}>
|
||||
<IconButton onClick={toggleWrapText} style={{paddingLeft: 0, paddingRight: 0}} disableRipple={true} disableFocusRipple={true}>
|
||||
<WrapTextIcon color={wrapText ? "success" : "secondary"}
|
||||
style={{cursor: "pointer"}}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
</MythicStyledTooltip>
|
||||
</>
|
||||
<MythicStyledTooltip title={autoScroll ? "Stop Auto Scroll" : "Auto Scroll"} >
|
||||
<IconButton onClick={toggleAutoScroll} style={{paddingLeft: 0, paddingRight: 0}} disableRipple={true} disableFocusRipple={true}>
|
||||
<HeightIcon color={autoScroll ? "success" : "secondary"}
|
||||
style={{cursor: "pointer"}}
|
||||
/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const InteractivePaginationBar = ({totalCount, currentPage, onSubmitPageChange, pageSize, expand}) => {
|
||||
const InteractivePaginationBar = ({totalCount, currentPage, onSubmitPageChange, pageSize, selectAllOutput}) => {
|
||||
const onChangePage = (event, value) => {
|
||||
onSubmitPageChange(value);
|
||||
};
|
||||
const pageCount = Math.max(1, Math.ceil(totalCount / pageSize));
|
||||
if(pageCount < 2){
|
||||
if(expand){
|
||||
return (
|
||||
<div style={{height: "50px"}}>
|
||||
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
if(pageCount < 2 || selectAllOutput){
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div style={{background: "transparent", display: "flex", justifyContent: "center", alignItems: "center", paddingBottom: "10px",}} >
|
||||
<div style={{background: "transparent", display: "flex", justifyContent: "center", alignItems: "center"}} >
|
||||
<Pagination count={pageCount} page={currentPage} variant="contained" color="primary"
|
||||
boundaryCount={2} onChange={onChangePage} style={{margin: "10px"}}
|
||||
showFirstButton showLastButton siblingCount={2}
|
||||
|
||||
@@ -1,642 +0,0 @@
|
||||
import React, {useEffect, useRef} from 'react';
|
||||
import {Button} from '@mui/material';
|
||||
import { MythicViewJSONAsTableDialog, MythicDialog } from '../../MythicComponents/MythicDialog';
|
||||
import { MythicDisplayTextDialog } from '../../MythicComponents/MythicDisplayTextDialog';
|
||||
import { ResponseDisplayTableDialogTable } from './ResponseDisplayTableDialogTable';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import 'react-virtualized/styles.css';
|
||||
import {TaskFromUIButton} from './TaskFromUIButton';
|
||||
import Grow from '@mui/material/Grow';
|
||||
import Popper from '@mui/material/Popper';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import MenuList from '@mui/material/MenuList';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import { copyStringToClipboard } from '../../utilities/Clipboard';
|
||||
import {snackActions} from '../../utilities/Snackbar';
|
||||
import {MythicStyledTooltip} from '../../MythicComponents/MythicStyledTooltip';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faList, faTrashAlt, faSkullCrossbones, faCamera, faSyringe, faFolder, faFolderOpen, faFileArchive, faCog, faFileWord, faFileExcel, faFilePowerpoint, faFilePdf, faDatabase, faKey, faFileCode, faDownload, faUpload, faFileImage, faCopy, faBoxOpen, faFileAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
// --------
|
||||
import {
|
||||
MaterialReactTable,
|
||||
useMaterialReactTable,
|
||||
} from 'material-react-table';
|
||||
|
||||
const onCopyToClipboard = (data) => {
|
||||
let result = copyStringToClipboard(data);
|
||||
if(result){
|
||||
snackActions.success("Copied text!");
|
||||
}else{
|
||||
snackActions.error("Failed to copy text");
|
||||
}
|
||||
}
|
||||
const getIconName = (iconName) => {
|
||||
switch(iconName.toLowerCase()){
|
||||
case "openfolder":
|
||||
case "folder":
|
||||
return faFolderOpen;
|
||||
case "closedfolder":
|
||||
return faFolder;
|
||||
case "archive":
|
||||
case "zip":
|
||||
return faFileArchive;
|
||||
case "diskimage":
|
||||
return faBoxOpen;
|
||||
case "executable":
|
||||
return faCog;
|
||||
case "word":
|
||||
return faFileWord;
|
||||
case "excel":
|
||||
return faFileExcel;
|
||||
case "powerpoint":
|
||||
return faFilePowerpoint;
|
||||
case "pdf":
|
||||
case "adobe":
|
||||
return faFilePdf;
|
||||
case "database":
|
||||
return faDatabase;
|
||||
case "key":
|
||||
return faKey;
|
||||
case "code":
|
||||
case "source":
|
||||
return faFileCode;
|
||||
case "download":
|
||||
return faDownload;
|
||||
case "upload":
|
||||
return faUpload;
|
||||
case "png":
|
||||
case "jpg":
|
||||
case "image":
|
||||
return faFileImage;
|
||||
case "list":
|
||||
return faList;
|
||||
case "delete":
|
||||
return faTrashAlt;
|
||||
case "inject":
|
||||
return faSyringe;
|
||||
case "kill":
|
||||
return faSkullCrossbones;
|
||||
case "camera":
|
||||
return faCamera;
|
||||
default:
|
||||
return faFileAlt;
|
||||
}
|
||||
}
|
||||
|
||||
const ResponseDisplayTableStringCell = ({cellData, rowData}) => {
|
||||
return (
|
||||
<div style={{...cellData?.cellStyle}}>
|
||||
{cellData?.startIcon?
|
||||
<MythicStyledTooltip title={cellData?.startIconHoverText || ""} >
|
||||
<FontAwesomeIcon icon={getIconName(cellData?.startIcon)} style={{marginRight: "5px", color: cellData?.startIconColor || ""}}/>
|
||||
</MythicStyledTooltip>
|
||||
: null
|
||||
}
|
||||
{cellData?.plaintextHoverText &&
|
||||
<MythicStyledTooltip title={cellData.plaintextHoverText}>
|
||||
<pre style={{display: "inline-block"}}>
|
||||
{cellData?.plaintext?.replaceAll?.("\n", "") || cellData?.plaintext}
|
||||
</pre>
|
||||
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
<pre style={{display: "inline-block"}}>
|
||||
{cellData?.plaintext?.replaceAll?.("\n", "") || cellData?.plaintext}
|
||||
</pre>
|
||||
|
||||
{ cellData?.endIcon &&
|
||||
<MythicStyledTooltip title={cellData?.endIconHoverText || ""}>
|
||||
<FontAwesomeIcon icon={getIconName(cellData?.endIcon)} style={{color: cellData?.endIconColor || ""}}/>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const ResponseDisplayTableDateCell = ({cellData, rowData}) => {
|
||||
return (
|
||||
<div style={{...(cellData?.cellStyle || null)}}>
|
||||
{cellData?.startIcon?
|
||||
<MythicStyledTooltip title={cellData?.startIconHoverText || ""} >
|
||||
<FontAwesomeIcon icon={getIconName(cellData?.startIcon)} style={{marginRight: "5px", color: cellData?.startIconColor || ""}}/>
|
||||
</MythicStyledTooltip>
|
||||
: null
|
||||
}
|
||||
{cellData?.plaintextHoverText? (
|
||||
<MythicStyledTooltip title={cellData.plaintextHoverText}>
|
||||
<pre style={{display: "inline-block"}}>
|
||||
{cellData?.plaintext?.toISOString?.()}
|
||||
</pre>
|
||||
|
||||
</MythicStyledTooltip>
|
||||
) : (
|
||||
<pre style={{display: "inline-block"}}>
|
||||
{cellData?.plaintext?.toISOString?.()}
|
||||
</pre>
|
||||
)}
|
||||
{cellData?.endIcon?
|
||||
<MythicStyledTooltip title={cellData?.endIconHoverText || ""}>
|
||||
<FontAwesomeIcon icon={getIconName(cellData?.endIcon)} style={{color: cellData?.endIconColor || ""}}/>
|
||||
</MythicStyledTooltip>: null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const ResponseDisplayTableNumberCell = ({cellData, rowData}) => {
|
||||
return (
|
||||
<div style={{...(cellData?.cellStyle || null)}}>
|
||||
{cellData?.startIcon?
|
||||
<MythicStyledTooltip title={cellData?.startIconHoverText || ""} >
|
||||
<FontAwesomeIcon icon={getIconName(cellData?.startIcon)} style={{marginRight: "5px", color: cellData?.startIconColor || ""}}/>
|
||||
</MythicStyledTooltip>
|
||||
: null
|
||||
}
|
||||
{cellData?.plaintextHoverText? (
|
||||
<MythicStyledTooltip title={cellData.plaintextHoverText}>
|
||||
<pre style={{display: "inline-block"}}>
|
||||
{cellData?.plaintext || " "}
|
||||
</pre>
|
||||
|
||||
</MythicStyledTooltip>
|
||||
) : (
|
||||
<pre style={{display: "inline-block"}}>
|
||||
{cellData?.plaintext || " "}
|
||||
</pre>
|
||||
)}
|
||||
{cellData?.endIcon?
|
||||
<MythicStyledTooltip title={cellData?.endIconHoverText || ""}>
|
||||
<FontAwesomeIcon icon={getIconName(cellData?.endIcon)} style={{color: cellData?.endIconColor || ""}}/>
|
||||
</MythicStyledTooltip>: null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export const getStringSize = ({cellData}) => {
|
||||
try{
|
||||
// process for getting human readable string from bytes: https://stackoverflow.com/a/18650828
|
||||
let bytes = parseInt(cellData["plaintext"]);
|
||||
if (cellData["plaintext"] === ''){
|
||||
return ""
|
||||
}
|
||||
if (bytes === 0){
|
||||
return "0 Bytes";
|
||||
}
|
||||
const decimals = 2;
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}catch(error){
|
||||
return cellData?.plaintext?.replaceAll?.("\n", "") || ""
|
||||
|
||||
}
|
||||
}
|
||||
const ResponseDisplayTableSizeCell = ({cellData, rowData}) => {
|
||||
return (
|
||||
<div style={{...(cellData?.cellStyle || null)}}>
|
||||
{cellData?.plaintextHoverText? (
|
||||
<MythicStyledTooltip title={cellData.plaintextHoverText} >
|
||||
<pre style={{display: "inline-block"}}>
|
||||
{getStringSize({cellData})}
|
||||
</pre>
|
||||
|
||||
</MythicStyledTooltip>
|
||||
) : (
|
||||
<pre style={{display: "inline-block"}}>
|
||||
{getStringSize({cellData})}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const ResponseDisplayTableActionCell = ({cellData, callback_id, rowData}) => {
|
||||
const theme = useTheme();
|
||||
const [openButton, setOpenButton] = React.useState(false);
|
||||
const [openTaskingButton, setOpenTaskingButton] = React.useState(false);
|
||||
const [openDictionaryButton, setOpenDictionaryButton] = React.useState(false);
|
||||
const [openStringButton, setOpenStringButton] = React.useState(false);
|
||||
const [openTableButton, setOpenTableButton] = React.useState(false);
|
||||
const dropdownAnchorRef = useRef(null);
|
||||
const [openDropdownButton, setOpenDropdownButton] = React.useState(false);
|
||||
const [taskingData, setTaskingData] = React.useState({});
|
||||
const handleClose = (event) => {
|
||||
if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
setOpenDropdownButton(false);
|
||||
};
|
||||
const handleMenuItemClick = (event, index) => {
|
||||
switch(cellData.button.value[index].type.toLowerCase()){
|
||||
case "task":
|
||||
setTaskingData(cellData.button.value[index]);
|
||||
setOpenTaskingButton(true);
|
||||
break;
|
||||
case "dictionary":
|
||||
setTaskingData(cellData.button.value[index]);
|
||||
setOpenDictionaryButton(true);
|
||||
break;
|
||||
case "string":
|
||||
setTaskingData(cellData.button.value[index]);
|
||||
setOpenStringButton(true);
|
||||
break;
|
||||
case "table":
|
||||
setTaskingData(cellData.button.value[index]);
|
||||
setOpenTableButton(true);
|
||||
break;
|
||||
}
|
||||
setOpenDropdownButton(false);
|
||||
};
|
||||
const finishedTasking = () => {
|
||||
setOpenButton(false);
|
||||
setOpenTaskingButton(false);
|
||||
setOpenDictionaryButton(false);
|
||||
setOpenStringButton(false);
|
||||
setOpenTableButton(false);
|
||||
setTaskingData({});
|
||||
}
|
||||
const finishedViewingData = () => {
|
||||
setOpenButton(false);
|
||||
setOpenTaskingButton(false);
|
||||
setOpenDictionaryButton(false);
|
||||
setOpenStringButton(false);
|
||||
setOpenTableButton(false);
|
||||
setTaskingData({});
|
||||
}
|
||||
const getButtonObject = () => {
|
||||
switch(cellData.button.type.toLowerCase()){
|
||||
case "dictionary":
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicStyledTooltip title={cellData?.button?.hoverText || " "} >
|
||||
<Button size="small" variant="contained" color="primary"
|
||||
onClick={() => setOpenButton(true)} disabled={cellData?.button?.disabled || false}
|
||||
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.startIconColor || ""}}/> : null}
|
||||
>{cellData?.button?.name || " "}</Button>
|
||||
</MythicStyledTooltip>
|
||||
{openButton &&
|
||||
<MythicDialog fullWidth={true} maxWidth="lg" open={openButton}
|
||||
onClose={()=>{setOpenButton(false);}}
|
||||
innerDialog={<MythicViewJSONAsTableDialog title={cellData?.button?.title || "Title Here"} leftColumn={cellData?.button?.leftColumnTitle || "Left Column"}
|
||||
rightColumn={cellData?.button?.rightColumnTitle || "Right Column"} value={cellData?.button?.value || {}} onClose={()=>{setOpenButton(false);}} />}
|
||||
/>
|
||||
}
|
||||
</React.Fragment>
|
||||
)
|
||||
case "string":
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicStyledTooltip title={cellData?.button?.hoverText || " "} >
|
||||
<Button size="small" variant="contained" color="primary"
|
||||
onClick={() => setOpenButton(true)} disabled={cellData?.button?.disabled || false}
|
||||
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.startIconColor || ""}}/> : null}
|
||||
>{cellData?.button?.name || " "}</Button>
|
||||
</MythicStyledTooltip>
|
||||
{openButton &&
|
||||
<MythicDisplayTextDialog fullWidth={true} maxWidth="lg" open={openButton} title={cellData?.button?.title || "Title Here"} value={cellData?.button?.value || ""}
|
||||
onClose={()=>{setOpenButton(false);}}
|
||||
/>
|
||||
}
|
||||
</React.Fragment>
|
||||
)
|
||||
case "table":
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicStyledTooltip title={cellData?.button?.hoverText || " "} >
|
||||
<Button size="small" variant="contained" color="primary"
|
||||
onClick={() => setOpenButton(true)} disabled={cellData?.button?.disabled || false}
|
||||
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.startIconColor || ""}}/> : null}
|
||||
>{cellData?.button?.name || " "}</Button>
|
||||
</MythicStyledTooltip>
|
||||
{openButton &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openButton}
|
||||
onClose={()=>{setOpenButton(false);}}
|
||||
innerDialog={<ResponseDisplayTableDialogTable title={cellData?.button?.title || "Title Here"}
|
||||
table={cellData?.button?.value || {}} callback_id={callback_id} onClose={()=>{setOpenButton(false);}} />}
|
||||
/>
|
||||
}
|
||||
</React.Fragment>
|
||||
)
|
||||
case "task":
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicStyledTooltip title={cellData?.button?.hoverText || " "}>
|
||||
<Button size="small" onClick={() => setOpenTaskingButton(true)} disabled={cellData?.button?.disabled || false} variant="contained" color="warning"
|
||||
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.startIconColor || ""}}/> : null}
|
||||
>{cellData?.button?.name || " "}</Button>
|
||||
</MythicStyledTooltip>
|
||||
{openTaskingButton &&
|
||||
<TaskFromUIButton ui_feature={cellData?.button?.ui_feature || " "}
|
||||
callback_id={callback_id}
|
||||
parameters={cellData?.button?.parameters || ""}
|
||||
openDialog={cellData?.button?.openDialog || false}
|
||||
getConfirmation={cellData?.button?.getConfirmation || false}
|
||||
acceptText={cellData?.button?.acceptText || "confirm"}
|
||||
selectCallback={cellData?.button?.selectCallback || false}
|
||||
onTasked={() => setOpenTaskingButton(false)}/>
|
||||
}
|
||||
</React.Fragment>
|
||||
)
|
||||
case "menu":
|
||||
return (
|
||||
<React.Fragment>
|
||||
{openTaskingButton &&
|
||||
<TaskFromUIButton ui_feature={taskingData.ui_feature}
|
||||
callback_id={callback_id}
|
||||
parameters={taskingData.parameters}
|
||||
openDialog={taskingData?.openDialog || false}
|
||||
getConfirmation={taskingData?.getConfirmation || false}
|
||||
acceptText={taskingData?.acceptText || "confirm"}
|
||||
selectCallback={taskingData?.selectCallback || false}
|
||||
onTasked={finishedTasking}/>
|
||||
}
|
||||
{openDictionaryButton &&
|
||||
<MythicDialog fullWidth={true} maxWidth="lg" open={openDictionaryButton}
|
||||
onClose={finishedViewingData}
|
||||
innerDialog={<MythicViewJSONAsTableDialog title={taskingData.title} leftColumn={taskingData.leftColumnTitle}
|
||||
rightColumn={taskingData.rightColumnTitle} value={taskingData.value} onClose={finishedViewingData} />}
|
||||
/>
|
||||
}
|
||||
{openStringButton &&
|
||||
<MythicDisplayTextDialog fullWidth={true} maxWidth="lg" open={openStringButton} title={taskingData?.title || "Title Here"} value={taskingData?.value || ""}
|
||||
onClose={finishedViewingData}
|
||||
/>
|
||||
}
|
||||
{openTableButton &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openTableButton}
|
||||
onClose={finishedViewingData}
|
||||
innerDialog={<ResponseDisplayTableDialogTable title={taskingData?.title || "Title Here"}
|
||||
table={taskingData?.value || {}} callback_id={callback_id} onClose={finishedViewingData} />}
|
||||
/>
|
||||
}
|
||||
<Button size="small" variant="contained" color="primary" ref={dropdownAnchorRef}
|
||||
onClick={() => setOpenDropdownButton(true)} disabled={cellData?.button?.disabled || false}
|
||||
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.startIconColor || ""}}/> : null}
|
||||
>{cellData?.button?.name || " "}</Button>
|
||||
<Popper open={openDropdownButton} anchorEl={dropdownAnchorRef.current} role={undefined} transition style={{zIndex: 4}}>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
|
||||
}}
|
||||
>
|
||||
<Paper variant="outlined" style={{backgroundColor: theme.palette.mode === 'dark' ? theme.palette.primary.dark : theme.palette.primary.light, color: "white"}}>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<MenuList id="split-button-menu" >
|
||||
{cellData.button.value.map((option, index) => (
|
||||
<MenuItem
|
||||
key={option.name + index}
|
||||
disabled={option.disabled}
|
||||
onClick={(event) => handleMenuItemClick(event, index)}
|
||||
>
|
||||
<MythicStyledTooltip title={option?.hoverText || (option.type === "task" ? "Task an Agent" : "Display Data")}>
|
||||
{option?.startIcon ? <FontAwesomeIcon icon={getIconName(option?.startIcon)} style={{color: option?.startIconColor || "", marginRight: "5px"}}/> : null}
|
||||
{option.name}
|
||||
</MythicStyledTooltip>
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div style={{...(rowData?.rowStyle || null), ...(cellData?.cellStyle || null)}}>
|
||||
{cellData?.plaintext ? cellData.plaintext : null}
|
||||
{cellData?.button ? (getButtonObject()) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export const ResponseDisplayMaterialReactTable = ({table, callback_id, expand, task}) =>{
|
||||
const theme = useTheme();
|
||||
const [allData, setAllData] = React.useState([]);
|
||||
const filterOutButtonsFromRowData = (data) => {
|
||||
let rowData = {};
|
||||
for(const key of Object.keys(data)){
|
||||
if(data[key]["plaintext"] !== undefined){
|
||||
rowData[key] = data[key]["plaintext"];
|
||||
}
|
||||
}
|
||||
return rowData;
|
||||
}
|
||||
const contextMenuOptions = [
|
||||
{
|
||||
name: 'Copy Row as JSON',
|
||||
click: ({event, columnIndex, rowIndex, data}) => {
|
||||
const filteredData = filterOutButtonsFromRowData(data);
|
||||
onCopyToClipboard(JSON.stringify(filteredData, null, 2));
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Copy Row as CSV',
|
||||
click: ({event, columnIndex, rowIndex, data}) => {
|
||||
const filteredData = filterOutButtonsFromRowData(data);
|
||||
let outputHeaders = "";
|
||||
let outputRow = "";
|
||||
for(const key of Object.keys(filteredData)){
|
||||
if(outputHeaders === ""){
|
||||
outputHeaders += key;
|
||||
} else {
|
||||
outputHeaders += "," + key;
|
||||
}
|
||||
if(outputRow === ""){
|
||||
outputRow += filteredData[key];
|
||||
}else{
|
||||
outputRow += "," + filteredData[key];
|
||||
}
|
||||
}
|
||||
onCopyToClipboard(outputHeaders + "\n" + outputRow);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Copy Row as TSV',
|
||||
click: ({event, columnIndex, rowIndex, data}) => {
|
||||
const filteredData = filterOutButtonsFromRowData(data);
|
||||
let outputHeaders = "";
|
||||
let outputRow = "";
|
||||
for(const key of Object.keys(filteredData)){
|
||||
if(outputHeaders === ""){
|
||||
outputHeaders += key;
|
||||
} else {
|
||||
outputHeaders += "\t" + key;
|
||||
}
|
||||
if(outputRow === ""){
|
||||
outputRow += filteredData[key];
|
||||
}else{
|
||||
outputRow += "\t" + filteredData[key];
|
||||
}
|
||||
}
|
||||
onCopyToClipboard(outputHeaders + "\n" + outputRow);
|
||||
}
|
||||
},
|
||||
];
|
||||
const columns = React.useMemo( () => table.headers.map( h => {
|
||||
return {
|
||||
accessorKey: `${h.plaintext}.plaintext`,
|
||||
header: h.plaintext,
|
||||
size: h.width,
|
||||
id: h.plaintext,
|
||||
enableClickToCopy: h.type !== 'button',
|
||||
filterVariant: h.type === 'number' || h.type === 'size' ? 'range' : 'text',
|
||||
enableResizing: h.type !== 'button',
|
||||
enableSorting: h.type !== 'button',
|
||||
enableColumnFilter: h.type !== 'button',
|
||||
grow: h.fillWidth,
|
||||
accessorFn: (row) => {
|
||||
if(h.type === "date"){
|
||||
return new Date(row[h.plaintext]?.plaintext || 0);
|
||||
}
|
||||
if(h.type === "number" || h.type === "size"){
|
||||
try{
|
||||
return Number(row[h.plaintext]?.plaintext);
|
||||
}catch(error){
|
||||
return row[h.plaintext]?.plaintext || "";
|
||||
}
|
||||
}
|
||||
return row[h.plaintext]?.plaintext || "";
|
||||
},
|
||||
Cell: ({cell, renderedCellValue}) => {
|
||||
let cellData = {...cell.row?.original?.[h.plaintext], plaintext: renderedCellValue};
|
||||
switch(h.type){
|
||||
case "string":
|
||||
return (
|
||||
<ResponseDisplayTableStringCell cellData={cellData} rowData={cell.row.original}/>
|
||||
)
|
||||
case "date":
|
||||
return (
|
||||
<ResponseDisplayTableDateCell cellData={cellData} rowData={cell.row.original}/>
|
||||
)
|
||||
case "size":
|
||||
return (
|
||||
<ResponseDisplayTableSizeCell cellData={cellData} rowData={cell.row.original}/>
|
||||
)
|
||||
case "button":
|
||||
return (
|
||||
<ResponseDisplayTableActionCell callback_id={callback_id} cellData={cellData} rowData={cell.row.original}/>
|
||||
)
|
||||
case "number":
|
||||
return (
|
||||
<ResponseDisplayTableNumberCell callback_id={callback_id} cellData={cellData} rowData={cell.row.original} />
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<ResponseDisplayTableStringCell cellData={cellData} rowData={cell.row.original}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}), [table.headers])
|
||||
|
||||
|
||||
useEffect( () => {
|
||||
setAllData([...table.rows]);
|
||||
}, [table.rows]);
|
||||
const materialReactTable = useMaterialReactTable({
|
||||
columns,
|
||||
data: allData,
|
||||
layoutMode: "grid",
|
||||
autoResetPageIndex: false,
|
||||
enableFacetedValues: true,
|
||||
enablePagination: true,
|
||||
enableBottomToolbar: true,
|
||||
enableStickyHeader: true,
|
||||
enableDensityToggle: false,
|
||||
enableColumnResizing: true,
|
||||
//enableColumnOrdering: true,
|
||||
//columnResizeMode: 'onEnd',
|
||||
defaultDisplayColumn: { enableResizing: true },
|
||||
muiTableContainerProps: { sx: { alignItems: "flex-start" } },
|
||||
mrtTheme: (theme) => ({
|
||||
baseBackgroundColor: theme.palette.background.default, //change default background color
|
||||
}),
|
||||
muiSearchTextFieldProps: {
|
||||
placeholder: 'Search loaded data',
|
||||
size: 'small',
|
||||
sx: { minWidth: '300px' },
|
||||
variant: 'outlined',
|
||||
},
|
||||
muiTableHeadCellProps: {
|
||||
sx: {
|
||||
border: '1px solid rgba(81, 81, 81, .5)',
|
||||
fontStyle: 'italic',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
style: {
|
||||
zIndex: 1,
|
||||
}
|
||||
},
|
||||
muiTableHeadRowProps: {
|
||||
sx: {}
|
||||
},
|
||||
muiTableBodyCellProps: ({ cell, table }) => {
|
||||
return {
|
||||
sx: {
|
||||
...cell.row.original[cell.column.id]?.cellStyle,
|
||||
padding: "0 0 0 10px",
|
||||
}
|
||||
}
|
||||
},
|
||||
muiTableBodyRowProps: ({ row }) => ({
|
||||
sx: {
|
||||
...row?.original?.rowStyle, padding: 0
|
||||
},
|
||||
style: {padding: 0}
|
||||
}),
|
||||
enableRowActions: false,
|
||||
/*
|
||||
renderRowActions: ({ row }) => (
|
||||
<Box>
|
||||
<IconButton onClick={() => console.info('Edit')}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={() => console.info('Delete')}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
),
|
||||
*/
|
||||
renderRowActionMenuItems: ({ row }) => contextMenuOptions.map( o => (
|
||||
<MenuItem key={o.name} onClick={() => {o.click({data: row.original})}}>{o.name}</MenuItem>
|
||||
)),
|
||||
muiTablePaperProps: {
|
||||
sx: { display: "flex", flexDirection: "column", width: "100%"}
|
||||
},
|
||||
muiTopToolbarProps: {
|
||||
sx: {backgroundColor: theme.materialReactTableHeader},
|
||||
},
|
||||
renderTopToolbarCustomActions: () => (
|
||||
<Typography variant="h5" >{table?.title || ""}</Typography>
|
||||
),
|
||||
renderEmptyRowsFallback: ({ table }) => (
|
||||
<div style={{display: "flex", width: "100%", height: "100%", justifyContent: "center", flexDirection: "column", alignItems: "center"}}>
|
||||
<Typography variant={"h4"} >
|
||||
{"No Data"}
|
||||
</Typography>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
const scrollContent = (node, isAppearing) => {
|
||||
// only auto-scroll if you issued the task
|
||||
document.getElementById(`scrolltotaskbottom${task.id}`)?.scrollIntoView?.({
|
||||
//behavior: "smooth",
|
||||
block: "end",
|
||||
inline: "nearest"
|
||||
})
|
||||
}
|
||||
React.useLayoutEffect( () => {
|
||||
scrollContent()
|
||||
}, []);
|
||||
return (
|
||||
<MaterialReactTable table={materialReactTable} />
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react';
|
||||
import {Typography, Link} from '@mui/material';
|
||||
import { Button, IconButton } from '@mui/material';
|
||||
import {useMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {modeOptions} from "../Search/PreviewFileStringDialog";
|
||||
import {GetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import AceEditor from 'react-ace';
|
||||
@@ -10,6 +9,7 @@ import {useTheme} from '@mui/material/styles';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import WrapTextIcon from '@mui/icons-material/WrapText';
|
||||
// https://github.com/ajaxorg/ace-builds/tree/master/src-min-noconflict
|
||||
import 'ace-builds/src-noconflict/mode-json';
|
||||
import 'ace-builds/src-noconflict/mode-csharp';
|
||||
import 'ace-builds/src-noconflict/mode-golang';
|
||||
@@ -21,26 +21,231 @@ import 'ace-builds/src-noconflict/mode-java';
|
||||
import 'ace-builds/src-noconflict/mode-javascript';
|
||||
import 'ace-builds/src-noconflict/theme-monokai';
|
||||
import 'ace-builds/src-noconflict/theme-xcode';
|
||||
import 'ace-builds/src-noconflict/mode-yaml';
|
||||
import 'ace-builds/src-noconflict/mode-toml';
|
||||
import 'ace-builds/src-noconflict/mode-swift';
|
||||
import 'ace-builds/src-noconflict/mode-sql';
|
||||
import 'ace-builds/src-noconflict/mode-rust';
|
||||
import 'ace-builds/src-noconflict/mode-powershell';
|
||||
import 'ace-builds/src-noconflict/mode-pgsql';
|
||||
import 'ace-builds/src-noconflict/mode-perl';
|
||||
import 'ace-builds/src-noconflict/mode-php';
|
||||
import 'ace-builds/src-noconflict/mode-objectivec';
|
||||
import 'ace-builds/src-noconflict/mode-nginx';
|
||||
import 'ace-builds/src-noconflict/mode-makefile';
|
||||
import 'ace-builds/src-noconflict/mode-kotlin';
|
||||
import 'ace-builds/src-noconflict/mode-dockerfile';
|
||||
import 'ace-builds/src-noconflict/mode-sh';
|
||||
import 'ace-builds/src-noconflict/mode-ini';
|
||||
import 'ace-builds/src-noconflict/mode-apache_conf';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import TabContext from '@mui/lab/TabContext';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import {previewFileQuery} from "../Search/FileMetaTable";
|
||||
import { useMutation, gql, useQuery } from '@apollo/client';
|
||||
import CodeIcon from '@mui/icons-material/Code';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import {b64DecodeUnicode} from "./ResponseDisplay";
|
||||
import {MythicDialog, TableRowSizeCell} from "../../MythicComponents/MythicDialog";
|
||||
import {Table, TableHead, TableRow, TableBody, TableCell, Paper} from '@mui/material';
|
||||
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
|
||||
import WarningOutlinedIcon from '@mui/icons-material/WarningOutlined';
|
||||
import {TagsDisplay, ViewEditTags} from "../../MythicComponents/MythicTag";
|
||||
import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery";
|
||||
import {ResponseDisplayScreenshotModal} from "./ResponseDisplayScreenshotModal";
|
||||
import initSQLJS from 'sql.js';
|
||||
import TabPanel from '@mui/lab/TabPanel';
|
||||
import TabList from '@mui/lab/TabList';
|
||||
import {Backdrop, CircularProgress} from '@mui/material';
|
||||
// Required to let webpack 4 know it needs to copy the wasm file to our assets
|
||||
// eslint-disable-next-line import/no-webpack-loader-syntax
|
||||
import sqlWasm from "!!file-loader?name=sql-wasm-[contenthash].wasm!sql.js/dist/sql-wasm.wasm";
|
||||
import {copyStringToClipboard} from "../../utilities/Clipboard";
|
||||
|
||||
|
||||
export const modeOptions = ["csharp", "golang", "html", "json", "markdown", "ruby", "python", "java",
|
||||
"javascript", "yaml", "toml", "swift", "sql", "rust", "powershell", "pgsql", "perl", "php", "objectivec",
|
||||
"nginx", "makefile", "kotlin", "dockerfile", "sh", "ini", "apache_conf"].sort();
|
||||
const fileMetaQuery = gql`
|
||||
query fileMetaStats($agent_file_id: String!){
|
||||
filemeta(where: {agent_file_id: {_eq: $agent_file_id}}){
|
||||
id
|
||||
host
|
||||
filename_text
|
||||
full_remote_path_text
|
||||
size
|
||||
agent_file_id
|
||||
complete
|
||||
total_chunks
|
||||
chunks_received
|
||||
task {
|
||||
display_id
|
||||
id
|
||||
}
|
||||
tags {
|
||||
tagtype {
|
||||
name
|
||||
color
|
||||
id
|
||||
}
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
return <DisplayMedia agent_file_id={media?.agent_file_id || ""} task={task} filename={media?.filename || undefined} expand={expand} />
|
||||
const displayType = mimeType(media?.filename);
|
||||
const [value, setValue] = React.useState(displayType !== undefined ? 0 : 1);
|
||||
const [fileMetaData, setFileMetaData] = React.useState({});
|
||||
const fetchedData = ({data}) => {
|
||||
if(data.filemeta.length > 0){
|
||||
setFileMetaData({
|
||||
id: data.filemeta[0].id,
|
||||
tags: data.filemeta[0].tags,
|
||||
filename: b64DecodeUnicode(data.filemeta[0].filename_text),
|
||||
full_remote_path: b64DecodeUnicode(data.filemeta[0].full_remote_path_text),
|
||||
host: data.filemeta[0].host,
|
||||
size: data.filemeta[0].size,
|
||||
agent_file_id: data.filemeta[0].agent_file_id,
|
||||
task_display_id: data.filemeta[0]?.task?.display_id,
|
||||
complete: data.filemeta[0].complete,
|
||||
total_chunks: data.filemeta[0].total_chunks,
|
||||
chunks_received: data.filemeta[0].chunks_received
|
||||
})
|
||||
} else {
|
||||
snackActions.warning("failed to find file specified")
|
||||
}
|
||||
}
|
||||
const fetchFileMetaData = useMythicLazyQuery(fileMetaQuery, {
|
||||
fetchPolicy: "no-cache",
|
||||
});
|
||||
React.useEffect( () => {
|
||||
fetchFileMetaData({variables: {agent_file_id: media?.agent_file_id}}).then( (data) => fetchedData(data)).catch((e) => console.log(e))
|
||||
}, [task]);
|
||||
const handleChange = (event, newValue) => {
|
||||
setValue(newValue);
|
||||
}
|
||||
return (
|
||||
<div style={{display: "flex", height: "100%", flexDirection: "column"}}>
|
||||
<DisplayFileMetaData fileMetaData={fileMetaData} />
|
||||
<AppBar color={'default'} position='static' className={"no-box-shadow"}>
|
||||
<Tabs
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
indicatorColor='secondary'
|
||||
textColor='primary'
|
||||
sx={{
|
||||
'& .Mui-selected': {
|
||||
color: "unset !important"
|
||||
}
|
||||
}}
|
||||
variant='scrollable'
|
||||
scrollButtons='auto'
|
||||
style={{maxWidth: '100%', width: '100%'}}
|
||||
aria-label='scrollable auto tabs example'>
|
||||
<Tab className={value === 0 ? "selectedCallback": ""} label={"Preview"}></Tab>
|
||||
<Tab className={value === 1 ? "selectedCallback": ""} label={"Text"}></Tab>
|
||||
<Tab className={value === 2 ? "selectedCallback": ""} label={"Hex"}></Tab>
|
||||
<Tab className={value === 3 ? "selectedCallback": ""} label={"Database"}></Tab>
|
||||
<MythicStyledTooltip title={"Download the file"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<Button style={{}} size={"small"} href={"/direct/download/" + media.agent_file_id}
|
||||
download color={"success"}>
|
||||
<DownloadIcon />
|
||||
</Button>
|
||||
</MythicStyledTooltip>
|
||||
</Tabs>
|
||||
</AppBar>
|
||||
<div hidden={value !== 0} style={{height: "100%", overflow: "auto"}} role='tabpanel' >
|
||||
{value === 0 &&
|
||||
<DisplayMedia agent_file_id={media?.agent_file_id || ""}
|
||||
task={task} filename={media?.filename || undefined}
|
||||
fileMetaData={fileMetaData}
|
||||
expand={expand} />
|
||||
}
|
||||
</div>
|
||||
<div hidden={value !== 1} style={{height: "100%", overflow: "auto"}} role='tabpanel' >
|
||||
{value === 1 &&
|
||||
<DisplayText agent_file_id={media?.agent_file_id || ""}
|
||||
task={task} filename={media?.filename || undefined}
|
||||
fileMetaData={fileMetaData}
|
||||
expand={expand} preview />
|
||||
}
|
||||
</div>
|
||||
<div hidden={value !== 2} style={{height: "100%", overflow: "auto"}} role='tabpanel' >
|
||||
{value === 2 &&
|
||||
<DisplayHex agent_file_id={media?.agent_file_id || ""}
|
||||
task={task} filename={media?.filename || undefined}
|
||||
fileMetaData={fileMetaData}
|
||||
expand={expand} />
|
||||
}
|
||||
|
||||
</div>
|
||||
<div hidden={value !== 3} style={{height: "100%", overflow: "auto"}} role='tabpanel' >
|
||||
{value === 3 &&
|
||||
<DisplayDatabase agent_file_id={media?.agent_file_id || ""}
|
||||
task={task} filename={media?.filename || undefined}
|
||||
fileMetaData={fileMetaData}
|
||||
expand={expand} />
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const textExtensionTypes = ["txt", "ps1", "php", "json", "yml", "yaml", "config", "cfg", "go",
|
||||
export const textExtensionTypes = ["txt", "ps1", "php", "json", "yml", "yaml", "config", "cfg", "go",
|
||||
"html", "xml", "js", "java", "conf", "cs", "rb", "toml", "sh", "md", "ini", "py", "kirbi", "bash_profile", "rc",
|
||||
"local", "gitconfig", "gitignore", "zsh_history", "bash_history", "ps", "psql_history", "lesshst", "gcloudignore",
|
||||
"pem", "boto", "zsh_profile", "pub", "python_history", "sqlite_history", "viminfo", "zprofile", "zshrc",
|
||||
"history", "historynew"
|
||||
"history", "historynew", "bashrc",
|
||||
];
|
||||
const knownTextFiles = ["config", "credentials", "known_hosts", "config_default", "id_rsa"];
|
||||
const imgExtensionTypes = ["png", "jpg", "gif", "jpeg", "pdf"];
|
||||
export const textExtensionTypesToSyntax = {
|
||||
"json": "json",
|
||||
"cs": "csharp",
|
||||
"go": "golang",
|
||||
"md": "markdown",
|
||||
"MD": "markdown",
|
||||
"rb": "ruby",
|
||||
"py": "python",
|
||||
"java": "java",
|
||||
"js": "javascript",
|
||||
"yml": "yaml",
|
||||
"yaml": "yaml",
|
||||
"toml": "toml",
|
||||
"swift": "swift",
|
||||
"psql": "sql",
|
||||
"rs": "rust",
|
||||
"ps1": "powershell",
|
||||
"prl": "perl",
|
||||
"php": "php",
|
||||
"m": "objectivec",
|
||||
"mm": "objectivec",
|
||||
"dockerfile": "dockerfile",
|
||||
"makefile": "makefile",
|
||||
"sh": "sh",
|
||||
"rc": "sh",
|
||||
"bashrc": "sh",
|
||||
"zshrc": "sh",
|
||||
"zprofile": "sh",
|
||||
"bash_profile": "sh",
|
||||
"ini": "ini",
|
||||
"conf": "apache_conf",
|
||||
"bash_history": "sh",
|
||||
"zsh_history": "sh",
|
||||
"python_history": "python",
|
||||
}
|
||||
const knownTextFiles = ["config", "credentials", "known_hosts", "config_default", "id_rsa", "dockerfile", "makefile"];
|
||||
const imgExtensionTypes = ["png", "jpg", "gif", "jpeg", "svg"];
|
||||
const renderExtensionTypes = ["pdf"];
|
||||
const mimeType = (path) => {
|
||||
if(!path){return undefined}
|
||||
let extension = path.split(".");
|
||||
if(extension.length > 0){
|
||||
if(extension.length > 1){
|
||||
extension = extension[extension.length - 1];
|
||||
if(imgExtensionTypes.includes(extension.toLowerCase())){
|
||||
return "image";
|
||||
}
|
||||
if(renderExtensionTypes.includes(extension.toLowerCase())){
|
||||
return "object";
|
||||
}
|
||||
if(textExtensionTypes.includes(extension.toLowerCase())){
|
||||
@@ -52,8 +257,9 @@ const mimeType = (path) => {
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
export const DisplayMedia = ({agent_file_id, filename, expand, task}) => {
|
||||
const showMediaSetting = useMythicSetting({setting_name: "showMedia", default_value: "true"});
|
||||
export const DisplayMedia = ({agent_file_id, filename, expand, task, fileMetaData}) => {
|
||||
const showMediaSetting = GetMythicSetting({setting_name: "showMedia", default_value: true});
|
||||
const [openScreenshot, setOpenScreenshot] = React.useState(false);
|
||||
const [showMedia, setShowMedia] = React.useState(showMediaSetting);
|
||||
const [fileData, setFileData] = React.useState({
|
||||
display: false,
|
||||
@@ -93,6 +299,11 @@ export const DisplayMedia = ({agent_file_id, filename, expand, task}) => {
|
||||
React.useLayoutEffect( () => {
|
||||
scrollContent()
|
||||
}, []);
|
||||
const clickedScreenshot = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setOpenScreenshot(true);
|
||||
}
|
||||
if(!showMedia){
|
||||
return (
|
||||
<>
|
||||
@@ -129,57 +340,185 @@ export const DisplayMedia = ({agent_file_id, filename, expand, task}) => {
|
||||
</object>
|
||||
)
|
||||
}
|
||||
if(fileData.display_type === "text"){
|
||||
if(fileData.display_type === "image"){
|
||||
return (
|
||||
<div style={{height: "100%", minHeight: "200px", width: "100%"}} >
|
||||
<DisplayText agent_file_id={agent_file_id} expand={expand} />
|
||||
</div>
|
||||
<>
|
||||
<img width={"100%"} onClick={clickedScreenshot}
|
||||
src={"/direct/view/" + agent_file_id} style={{cursor: "zoom-in", width: "100%"}}>
|
||||
</img>
|
||||
{openScreenshot &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openScreenshot}
|
||||
onClose={() => {
|
||||
setOpenScreenshot(false);
|
||||
}}
|
||||
innerDialog={<ResponseDisplayScreenshotModal images={[agent_file_id]}
|
||||
onClose={() => {
|
||||
setOpenScreenshot(false);
|
||||
}}/>}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
if (fileData.display_type === "text") {
|
||||
return (
|
||||
<div style={{height: "100%", minHeight: "100px", width: "100%"}}>
|
||||
<DisplayText agent_file_id={agent_file_id} expand={expand} filename={filename}
|
||||
fileMetaData={fileMetaData}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const MaxRenderSize = 2000000;
|
||||
const DisplayText = ({agent_file_id, expand}) => {
|
||||
const MaxRenderSize = 2000000; // 2MB
|
||||
const DisplayFileMetaData = ({fileMetaData}) => {
|
||||
return (
|
||||
<Table style={{marginLeft: "0px", width: "100%", tableLayout: "fixed"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "7rem"}}>Size</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>Host</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>File</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>Path</MythicStyledTableCell>
|
||||
<MythicStyledTableCell style={{width: "5rem"}}>Task</MythicStyledTableCell>
|
||||
<MythicStyledTableCell style={{}}>Tags</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell>
|
||||
<TableRowSizeCell cellData={fileMetaData.size}/>
|
||||
{fileMetaData.complete ? null : (
|
||||
<>
|
||||
<br/>
|
||||
<Typography style={{display: "inline-block", color: "red"}}>
|
||||
{fileMetaData.chunks_received}
|
||||
</Typography>
|
||||
/
|
||||
<Typography style={{display: "inline-block"}}>
|
||||
{fileMetaData.total_chunks}
|
||||
</Typography>
|
||||
<Typography style={{display: "inline-block", whiteSpace: "pre"}}>
|
||||
{" Chunks"}
|
||||
</Typography>
|
||||
</>
|
||||
|
||||
)}
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell style={{wordBreak: "break-all"}}>{fileMetaData.host}</MythicStyledTableCell>
|
||||
<MythicStyledTableCell style={{wordBreak: "break-all"}}>{fileMetaData.filename}</MythicStyledTableCell>
|
||||
<MythicStyledTableCell style={{wordBreak: "break-all"}}>{fileMetaData.full_remote_path}</MythicStyledTableCell>
|
||||
<MythicStyledTableCell><Link style={{wordBreak: "break-all"}}
|
||||
color="textPrimary" underline="always"
|
||||
target="_blank" href={"/new/task/" + fileMetaData.task_display_id}>
|
||||
{fileMetaData.task_display_id}
|
||||
</Link></MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<ViewEditTags target_object={"filemeta_id"} target_object_id={fileMetaData.id} />
|
||||
<TagsDisplay tags={fileMetaData.tags}/>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
const DisplayText = ({agent_file_id, expand, filename, preview, fileMetaData}) => {
|
||||
const theme = useTheme();
|
||||
const [mode, setMode] = React.useState("html");
|
||||
const [content, setContent] = React.useState("");
|
||||
const [wrapText, setWrapText] = React.useState(true);
|
||||
React.useEffect( () => {
|
||||
|
||||
fetch('/direct/view/' + agent_file_id).then((response) => {
|
||||
if(response.status !== 200){
|
||||
snackActions.warning("Failed to fetch contents from Mythic");
|
||||
return;
|
||||
}
|
||||
response.text().then(data => {
|
||||
if(data.length > MaxRenderSize){
|
||||
snackActions.warning("File too large (> 2MB), truncating the render");
|
||||
const [previewFileString] = useMutation(previewFileQuery, {
|
||||
onCompleted: (data) => {
|
||||
if(data.previewFile.status === "success"){
|
||||
setContent(b64DecodeUnicode(data.previewFile.contents));
|
||||
if(data.previewFile.size > 512000){
|
||||
setLimitedPreviewWarning(true);
|
||||
}
|
||||
}else{
|
||||
console.log(data.previewFile.error);
|
||||
if(data.previewFile.error !== "EOF"){
|
||||
snackActions.error(data.previewFile.error);
|
||||
}
|
||||
setContent(data.substring(0, MaxRenderSize));
|
||||
}).catch(error => {
|
||||
snackActions.warning("Error getting contents from server: " + error.toString());
|
||||
console.log("Error trying to get json response", error, response);
|
||||
});
|
||||
}).catch(error => {
|
||||
if(error.toString() === "TypeError: Failed to fetch"){
|
||||
snackActions.warning("Please refresh and accept the SSL connection error");
|
||||
} else {
|
||||
snackActions.warning("Error talking to server: " + error.toString());
|
||||
}
|
||||
console.log("There was an error!", error);
|
||||
});
|
||||
}, []);
|
||||
},
|
||||
onError: (data) => {
|
||||
console.log(data);
|
||||
snackActions.error(data);
|
||||
}
|
||||
});
|
||||
const [limitedPreviewWarning, setLimitedPreviewWarning] = React.useState(false);
|
||||
const currentContentRef = React.useRef();
|
||||
React.useEffect( () => {
|
||||
if(preview){
|
||||
// get first 512KB
|
||||
previewFileString({variables: {file_id: agent_file_id}})
|
||||
}else{
|
||||
// get entire file
|
||||
fetch('/direct/view/' + agent_file_id).then((response) => {
|
||||
if(response.status !== 200){
|
||||
snackActions.warning("Failed to fetch contents from Mythic");
|
||||
return;
|
||||
}
|
||||
response.text().then(data => {
|
||||
if(data.length > MaxRenderSize){
|
||||
snackActions.warning("File too large (> 2MB), truncating the render");
|
||||
setLimitedPreviewWarning(true);
|
||||
setContent(data.substring(0, MaxRenderSize));
|
||||
return;
|
||||
}
|
||||
try{
|
||||
let cont = JSON.stringify(JSON.parse(data), null, 2);
|
||||
setContent(cont);
|
||||
}catch(error){
|
||||
setContent(data.substring(0, MaxRenderSize));
|
||||
}
|
||||
}).catch(error => {
|
||||
snackActions.warning("Error getting contents from server: " + error.toString());
|
||||
console.log("Error trying to get json response", error, response);
|
||||
});
|
||||
}).catch(error => {
|
||||
if(error.toString() === "TypeError: Failed to fetch"){
|
||||
snackActions.warning("Please refresh and accept the SSL connection error");
|
||||
} else {
|
||||
snackActions.warning("Error talking to server: " + error.toString());
|
||||
}
|
||||
console.log("There was an error!", error);
|
||||
});
|
||||
}
|
||||
if(filename){
|
||||
let extension = filename.split(".");
|
||||
if(extension.length > 1){
|
||||
extension = extension[extension.length - 1];
|
||||
if(textExtensionTypesToSyntax[extension]){
|
||||
setMode(textExtensionTypesToSyntax[extension]);
|
||||
}
|
||||
} else {
|
||||
if(textExtensionTypesToSyntax[filename.toLowerCase()]){
|
||||
setMode(textExtensionTypesToSyntax[filename.toLowerCase()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [agent_file_id, filename, fileMetaData]);
|
||||
const onChangeMode = (event) => {
|
||||
setMode(event.target.value);
|
||||
}
|
||||
const toggleWrapText = () => {
|
||||
setWrapText(!wrapText);
|
||||
}
|
||||
const formatJSON = () => {
|
||||
try{
|
||||
let tmp = JSON.stringify(JSON.parse(currentContentRef.current?.editor?.getValue()), null, 2);
|
||||
setContent(tmp);
|
||||
setMode("json");
|
||||
}catch(error){
|
||||
snackActions.warning("Failed to reformat as JSON")
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div style={{display: "flex", height: "100%", flexDirection: "column"}}>
|
||||
<div style={{display: "inline-flex", flexDirection: "row"}}>
|
||||
<FormControl sx={{ display: "inline-block" }} size="small" color={"secondary"}>
|
||||
<div style={{display: "inline-flex", flexDirection: "row", alignItems: "center"}}>
|
||||
<FormControl sx={{ display: "inline-block", marginLeft: "10px" }} size="small" color={"secondary"}>
|
||||
<TextField
|
||||
select
|
||||
label={"Syntax"}
|
||||
@@ -204,9 +543,23 @@ const DisplayText = ({agent_file_id, expand}) => {
|
||||
/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Auto format JSON"} >
|
||||
<IconButton onClick={formatJSON} style={{}}>
|
||||
<CodeIcon color={"info"} style={{cursor: "pointer"}} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
{limitedPreviewWarning &&
|
||||
<>
|
||||
<MythicStyledTooltip title={preview ? "Only first 512KB of file is shown" :
|
||||
"Only first 2MB of file is shown"}>
|
||||
<WarningOutlinedIcon style={{marginLeft: "5px"}} color={"warning"} />
|
||||
</MythicStyledTooltip>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
<div style={{display: "flex", flexGrow: 1, height: "100%"}}>
|
||||
<AceEditor
|
||||
ref={currentContentRef}
|
||||
mode={mode}
|
||||
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
|
||||
fontSize={14}
|
||||
@@ -231,4 +584,326 @@ const DisplayText = ({agent_file_id, expand}) => {
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const DisplayHex = ({agent_file_id, expand, fileMetaData}) => {
|
||||
const theme = useTheme();
|
||||
const [content, setContent] = React.useState("");
|
||||
const [previewFileString] = useMutation(previewFileQuery, {
|
||||
onCompleted: (previewData) => {
|
||||
if(previewData.previewFile.status === "success"){
|
||||
let data = atob(previewData.previewFile.contents);
|
||||
let newData = "";
|
||||
let stringData = "";
|
||||
for(let i = 0; i < data.length; i++){
|
||||
let char = data[i].charCodeAt(0).toString(16);
|
||||
if(char.length === 1){
|
||||
newData += "0" + char;
|
||||
}else{
|
||||
newData += char;
|
||||
}
|
||||
if(data[i] === "\n"){
|
||||
stringData += "\\n";
|
||||
}else if(data[i] === "\r"){
|
||||
stringData += "\\r";
|
||||
}else{
|
||||
stringData += data[i];
|
||||
}
|
||||
if((i+1) % 4 === 0){
|
||||
newData += " ";
|
||||
}
|
||||
if((i+1) % 32 === 0){
|
||||
newData += ": " + stringData + "\n";
|
||||
stringData = "";
|
||||
}
|
||||
}
|
||||
setContent(newData);
|
||||
}else{
|
||||
snackActions.error(previewData.previewFile.error)
|
||||
}
|
||||
},
|
||||
onError: (data) => {
|
||||
console.log(data);
|
||||
snackActions.error(data);
|
||||
}
|
||||
});
|
||||
React.useEffect( () => {
|
||||
previewFileString({variables: {file_id: agent_file_id}})
|
||||
}, [agent_file_id])
|
||||
return (
|
||||
<div style={{display: "flex", height: "100%", flexDirection: "column"}}>
|
||||
<div style={{display: "flex", flexGrow: 1, height: "100%"}}>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
|
||||
fontSize={14}
|
||||
showGutter={true}
|
||||
//onLoad={onLoad}
|
||||
highlightActiveLine={false}
|
||||
showPrintMargin={false}
|
||||
value={content}
|
||||
height={expand ? "100%": undefined}
|
||||
maxLines={expand ? undefined : 20}
|
||||
width={"100%"}
|
||||
//autoScrollEditorIntoView={true}
|
||||
minLines={2}
|
||||
//maxLines={props.expand ? 50 : 20}
|
||||
setOptions={{
|
||||
showLineNumbers: true,
|
||||
tabSize: 4,
|
||||
useWorker: false
|
||||
}}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const MAX_ROWS = 1000;
|
||||
const DisplayDatabase = ({agent_file_id, expand, fileMetaData}) => {
|
||||
const theme = useTheme();
|
||||
const queryCountRef = React.useRef(1);
|
||||
const currentContentRef = React.useRef();
|
||||
const [selectedTab, setSelectedTab] = React.useState(0);
|
||||
const [sql, setSql] = React.useState(null);
|
||||
const [query, setQuery] = React.useState("select * from cookies;");
|
||||
const [results, setResults] = React.useState([]);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const onChangeText = (data) => {
|
||||
setQuery(data);
|
||||
}
|
||||
const removeQuery = (event, index) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if(selectedTab === index){
|
||||
if(index !== 0){
|
||||
setSelectedTab(index-1);
|
||||
}
|
||||
}
|
||||
setResults(results.toSpliced(index, 1));
|
||||
}
|
||||
const onSubmitQuery = (event) => {
|
||||
if(event){
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
if(sql){
|
||||
try{
|
||||
const result = sql.exec(query);
|
||||
//console.log(result);
|
||||
if(result.length > 0){
|
||||
setResults([...results, {name: `Query: ${queryCountRef.current}`, query: query, results: result[0]}]);
|
||||
setSelectedTab(queryCountRef.current - 1);
|
||||
queryCountRef.current += 1;
|
||||
}
|
||||
}catch(e){
|
||||
console.log(e);
|
||||
snackActions.error(e.message);
|
||||
}
|
||||
} else {
|
||||
console.log(sql);
|
||||
snackActions.error("Failed to load as SQL Database");
|
||||
}
|
||||
}
|
||||
const onChangeTab = (_, v) => {
|
||||
const tabQuery = results[v]?.query;
|
||||
setQuery(tabQuery);
|
||||
setSelectedTab(v);
|
||||
}
|
||||
const onCopyToClipboard = (data) => {
|
||||
let result = copyStringToClipboard(data);
|
||||
if(result){
|
||||
snackActions.success("Copied text!");
|
||||
}else{
|
||||
snackActions.error("Failed to copy text");
|
||||
}
|
||||
}
|
||||
const onSaveOutputCSV = (result) => {
|
||||
const filteredData = result.results.values;
|
||||
let outputHeaders = "";
|
||||
let outputRow = "";
|
||||
outputHeaders = result.results.columns.join(",");
|
||||
for(let i = 0; i < filteredData.length; i++){
|
||||
outputRow += filteredData[i].join(",") + "\n"
|
||||
}
|
||||
onCopyToClipboard(outputHeaders + "\n" + outputRow);
|
||||
}
|
||||
const onSaveOutputPrettyPrint = (result) => {
|
||||
let tableData = result.results.values;
|
||||
if(tableData.length === 0){return}
|
||||
let outputHeaders = result.results.columns;
|
||||
let outputHeadersMaxLength = outputHeaders.reduce( (prev, cur) => {
|
||||
return [...prev, cur.length]
|
||||
}, []);
|
||||
tableData.map( cur => {
|
||||
for(let i = 0; i < outputHeaders.length; i++){
|
||||
if(outputHeadersMaxLength[i] < String(cur[i]).length){
|
||||
outputHeadersMaxLength[i] = String(cur[i]).length;
|
||||
}
|
||||
}
|
||||
});
|
||||
let allOutput = "";
|
||||
for(let i = 0; i < outputHeaders.length; i++){
|
||||
let remainingSpace = Math.max(0, outputHeadersMaxLength[i] - outputHeaders[i].length);
|
||||
allOutput += " " + outputHeaders[i].toUpperCase() + " ".repeat(remainingSpace + 2);
|
||||
}
|
||||
allOutput += "\n";
|
||||
for(let i = 0; i < outputHeaders.length; i++){
|
||||
let remainingSpace = Math.max(0, outputHeadersMaxLength[i] - outputHeaders[i].length);
|
||||
allOutput += " " + "-".repeat(outputHeaders[i].length) + " ".repeat(remainingSpace + 2);
|
||||
}
|
||||
allOutput += "\n";
|
||||
for(let i = 0; i < tableData.length; i++){
|
||||
for(let j = 0; j < outputHeaders.length; j++){
|
||||
let remainingSpace = Math.max(0, outputHeadersMaxLength[j] - String(tableData[i][j]).length);
|
||||
allOutput += "| " + String(tableData[i][j]) + " ".repeat(remainingSpace) + " ";
|
||||
}
|
||||
allOutput += " |\n"
|
||||
}
|
||||
onCopyToClipboard(allOutput);
|
||||
}
|
||||
React.useEffect( () => {
|
||||
async function initialize(){
|
||||
const newSQL = await initSQLJS({locateFile: () => sqlWasm});
|
||||
fetch('/direct/view/' + agent_file_id).then((response) => {
|
||||
if(response.status !== 200){
|
||||
setLoading(false);
|
||||
snackActions.warning("Failed to fetch contents from Mythic");
|
||||
return;
|
||||
}
|
||||
response.arrayBuffer().then( data => {
|
||||
const SQL = new newSQL.Database(new Uint8Array(data));
|
||||
if(SQL){
|
||||
setSql(SQL);
|
||||
}
|
||||
setLoading(false);
|
||||
}).catch(error => {
|
||||
setLoading(false);
|
||||
snackActions.warning("Error getting contents from server: " + error.toString());
|
||||
console.log("Error trying to get json response", error, response);
|
||||
});
|
||||
}).catch(error => {
|
||||
if(error.toString() === "TypeError: Failed to fetch"){
|
||||
snackActions.warning("Please refresh and accept the SSL connection error");
|
||||
} else {
|
||||
snackActions.warning("Error talking to server: " + error.toString());
|
||||
}
|
||||
setLoading(false);
|
||||
console.log("There was an error!", error);
|
||||
});
|
||||
}
|
||||
initialize();
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
// Execute query on Ctrl+Enter or Cmd+Enter when in the SQL editor
|
||||
if (e.shiftKey && e.key === 'Enter') {
|
||||
onSubmitQuery(e);
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [onSubmitQuery]);
|
||||
return (
|
||||
<div style={{display: "flex", height: "100%", flexDirection: "column", width: "100%", position: "relative"}}>
|
||||
<Backdrop open={loading} onClick={()=>{setLoading(false);}} style={{zIndex: 2, position: "absolute"}}>
|
||||
<CircularProgress color="inherit" disableShrink />
|
||||
</Backdrop>
|
||||
<div style={{display: "inline-flex", flexDirection: "row", alignItems: "center", borderBottom: "1px solid grey"}}>
|
||||
<AceEditor
|
||||
className={"roundedBottomCorners"}
|
||||
ref={currentContentRef}
|
||||
mode={'sql'}
|
||||
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
|
||||
fontSize={14}
|
||||
showGutter={ true}
|
||||
onChange={onChangeText}
|
||||
highlightActiveLine={false}
|
||||
showPrintMargin={false}
|
||||
value={query}
|
||||
height={"100%"}
|
||||
maxLines={3}
|
||||
width={"100%"}
|
||||
wrapEnabled={true}
|
||||
minLines={2}
|
||||
setOptions={{
|
||||
showLineNumbers: true,
|
||||
tabSize: 4,
|
||||
useWorker: false,
|
||||
showInvisibles: false,
|
||||
}}/>
|
||||
<Button color={"success"} variant={"contained"} onClick={onSubmitQuery} >Query</Button>
|
||||
</div>
|
||||
<TabContext style={{width: "100%", height: "100%", borderBottom: "1px solid grey", overflow: "auto"}} value={selectedTab}>
|
||||
<TabList onChange={onChangeTab} indicatorColor='secondary'
|
||||
textColor='primary'
|
||||
sx={{
|
||||
'& .Mui-selected': {
|
||||
color: "unset !important"
|
||||
}
|
||||
}}>
|
||||
{results.map((result, index) => (
|
||||
<Tab key={result.query + index} style={{padding: 0, paddingLeft: "10px", paddingRight: "10px"}} label={
|
||||
<div style={{display: "inline-block"}}>
|
||||
{result.name}
|
||||
<IconButton style={{padding: 0, marginLeft: "5px", marginRight: "5px", marginBottom: "5px"}}
|
||||
color={"error"}
|
||||
onClick={(e) => removeQuery(e, index)}>
|
||||
x
|
||||
</IconButton>
|
||||
</div>} value={index} className={selectedTab === index ? "selectedCallback" : ""} />
|
||||
))}
|
||||
</TabList>
|
||||
|
||||
{results.map((result, index) => (
|
||||
<div key={index} style={{display: selectedTab === index ? "flex" : "none",
|
||||
width: "100%", height: "100%", overflow: "auto",
|
||||
flexDirection: "column"}}>
|
||||
{result?.results?.values?.length >= MAX_ROWS &&
|
||||
<Paper key={"overflow" + index} style={{display: selectedTab === index ? "flex" : "none",
|
||||
justifyContent: "center", hidden: selectedTab !== index}}>
|
||||
<WarningOutlinedIcon color={"warning"}></WarningOutlinedIcon>
|
||||
{"Output is truncated to first " + MAX_ROWS + " rows"}
|
||||
<WarningOutlinedIcon color={"warning"}></WarningOutlinedIcon>
|
||||
</Paper>
|
||||
}
|
||||
<div key={"output" + index} style={{display: selectedTab === index ? "flex" : "none", alignItems: "center", width: "100%"}}>
|
||||
{"Save Output As: "}
|
||||
<Button variant={"outlined"} style={{marginLeft: "10px"}} onClick={() => onSaveOutputCSV(result)} >CSV</Button>
|
||||
<Button variant={"outlined"} style={{marginLeft: "10px"}} onClick={() => onSaveOutputPrettyPrint(result)} >Pretty Print</Button>
|
||||
</div>
|
||||
<TabPanel value={index} key={"tabpanel" + index}
|
||||
style={{padding: 0, height: "100%", width: "100%", overflow: "auto", position: "relative",
|
||||
display: selectedTab === index ? "flex" : "none",
|
||||
flexDirection: "column"}} >
|
||||
<DisplayDatabaseResult result={result?.results} />
|
||||
</TabPanel>
|
||||
</div>
|
||||
))}
|
||||
</TabContext>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const DisplayDatabaseResult = ({result}) => {
|
||||
const [columns, setColumns] = React.useState(result.columns);
|
||||
const [rows, setRows] = React.useState(result.values?.slice(0, MAX_ROWS));
|
||||
return (
|
||||
<Table stickyHeader style={{width: "100%", height: "100%", position: "absolute"}}>
|
||||
<TableHead >
|
||||
<TableRow>
|
||||
{columns.map((column, index) => (
|
||||
<TableCell key={"header" + index}>{column}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row, index1) => (
|
||||
<TableRow key={"row" + index1} hover>
|
||||
{columns.map((_, index2) => (
|
||||
<TableCell key={"row" + index1+ "cell" + index2}>
|
||||
{row[index2]}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
@@ -1,45 +1,53 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import AceEditor from 'react-ace';
|
||||
import 'ace-builds/src-noconflict/mode-json';
|
||||
import 'ace-builds/src-noconflict/theme-monokai';
|
||||
import 'ace-builds/src-noconflict/theme-xcode';
|
||||
import "ace-builds/src-noconflict/ext-searchbox";
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import {modeOptions} from "../Search/PreviewFileStringDialog";
|
||||
import {modeOptions} from "./ResponseDisplayMedia";
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import WrapTextIcon from '@mui/icons-material/WrapText';
|
||||
import 'ace-builds/src-noconflict/mode-csharp';
|
||||
import 'ace-builds/src-noconflict/mode-golang';
|
||||
import 'ace-builds/src-noconflict/mode-html';
|
||||
import 'ace-builds/src-noconflict/mode-markdown';
|
||||
import 'ace-builds/src-noconflict/mode-ruby';
|
||||
import 'ace-builds/src-noconflict/mode-python';
|
||||
import 'ace-builds/src-noconflict/mode-java';
|
||||
import 'ace-builds/src-noconflict/mode-javascript';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import { IconButton } from '@mui/material';
|
||||
import { IconButton, Select } from '@mui/material';
|
||||
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
|
||||
import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
|
||||
import { useReactiveVar } from '@apollo/client';
|
||||
import { meState } from '../../../cache';
|
||||
import CodeIcon from '@mui/icons-material/Code';
|
||||
import {GetOutputFormatAll} from "./ResponseDisplayInteractive";
|
||||
import PaletteIcon from '@mui/icons-material/Palette';
|
||||
import Input from '@mui/material/Input';
|
||||
|
||||
const MaxRenderSize = 2000000;
|
||||
export const ResponseDisplayPlaintext = (props) =>{
|
||||
const theme = useTheme();
|
||||
const me = useReactiveVar(meState);
|
||||
const currentContentRef = React.useRef();
|
||||
const [plaintextView, setPlaintextView] = React.useState("");
|
||||
const [mode, setMode] = React.useState("html");
|
||||
const [wrapText, setWrapText] = React.useState(true);
|
||||
const initialMode = props?.initial_mode || "html";
|
||||
const [mode, setMode] = React.useState(initialMode);
|
||||
const [wrapText, setWrapText] = React.useState(props?.wrap_text === undefined ? true : props.wrap_text);
|
||||
const [showOptions, setShowOptions] = React.useState(false);
|
||||
const [renderColors, setRenderColors] = React.useState(props?.render_colors === undefined ? false : props.render_colors);
|
||||
const onChangeText = (data) => {
|
||||
if(props.onChangeContent){
|
||||
props?.onChangeContent(data);
|
||||
}
|
||||
setPlaintextView(data);
|
||||
}
|
||||
useEffect( () => {
|
||||
if(props.plaintext.length > MaxRenderSize){
|
||||
snackActions.warning("Response too large (> 2MB), truncating the render. Download task output to view entire response.");
|
||||
setPlaintextView(props.plaintext.substring(0, MaxRenderSize));
|
||||
} else {
|
||||
try{
|
||||
const newPlaintext = JSON.stringify(JSON.parse(String(props.plaintext)), null, 4);
|
||||
setPlaintextView(newPlaintext);
|
||||
setMode("json");
|
||||
if(props.autoFormat === undefined || props.autoFormat){
|
||||
const newPlaintext = JSON.stringify(JSON.parse(String(props.plaintext)), null, 4);
|
||||
setPlaintextView(newPlaintext);
|
||||
setMode("json");
|
||||
} else {
|
||||
setPlaintextView(String(props.plaintext));
|
||||
}
|
||||
}catch(error){
|
||||
setPlaintextView(String(props.plaintext));
|
||||
}
|
||||
@@ -51,31 +59,63 @@ export const ResponseDisplayPlaintext = (props) =>{
|
||||
const toggleWrapText = () => {
|
||||
setWrapText(!wrapText);
|
||||
}
|
||||
const formatJSON = () => {
|
||||
try{
|
||||
let tmp = JSON.stringify(JSON.parse(currentContentRef.current?.editor?.getValue()), null, 2);
|
||||
setPlaintextView(tmp);
|
||||
setMode("json");
|
||||
}catch(error){
|
||||
snackActions.warning("Failed to reformat as JSON")
|
||||
}
|
||||
}
|
||||
const onChangeShowOptions = (e) => {
|
||||
setShowOptions(!showOptions);
|
||||
}
|
||||
const scrollContent = (node, isAppearing) => {
|
||||
// only auto-scroll if you issued the task
|
||||
document.getElementById(`scrolltotaskbottom${props.task.id}`)?.scrollIntoView({
|
||||
//behavior: "smooth",
|
||||
block: "end",
|
||||
inline: "nearest"
|
||||
})
|
||||
if(props?.task?.operator?.username === (me?.user?.username || "")){
|
||||
let el = document.getElementById(`taskingPanel${props.task.callback_id}`);
|
||||
if(props.expand || props.displayType === "console"){
|
||||
el = document.getElementById(`taskingPanelConsole${props.task.callback_id}`);
|
||||
}
|
||||
if(el && el.scrollHeight - el.scrollTop - el.clientHeight < 600){
|
||||
document.getElementById(`scrolltotaskbottom${props.task.id}`)?.scrollIntoView({
|
||||
//behavior: "smooth",
|
||||
block: "end",
|
||||
inline: "nearest"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
React.useLayoutEffect( () => {
|
||||
scrollContent()
|
||||
}, []);
|
||||
React.useEffect( () => {
|
||||
setMode(props?.initial_mode || "html");
|
||||
}, [props?.initial_mode]);
|
||||
return (
|
||||
<div style={{display: "flex", height: "100%", flexDirection: "column"}}>
|
||||
{showOptions &&
|
||||
<div style={{display: "inline-flex", flexDirection: "row"}}>
|
||||
<FormControl sx={{ display: "inline-block" }} size="small">
|
||||
<div style={{display: "inline-flex", flexDirection: "row", alignItems: "center",
|
||||
color: theme.outputTextColor,
|
||||
backgroundColor: theme.outputBackgroundColor + (theme.palette.mode === 'dark' ? "D0" : "20"),}}>
|
||||
<FormControl sx={{ display: "inline-block", marginLeft: "10px", color: theme.outputTextColor, }} size="small">
|
||||
<TextField
|
||||
label={"Syntax"}
|
||||
select
|
||||
margin={"dense"}
|
||||
size={"small"}
|
||||
style={{display: "inline-block", width: "100%"}}
|
||||
sx={{
|
||||
// Target the icon element directly within the Select
|
||||
'.MuiSelect-icon': {
|
||||
color: theme.outputTextColor, // Set your desired color
|
||||
},
|
||||
'.MuiInputBase-root': {
|
||||
color: theme.outputTextColor,
|
||||
}
|
||||
}}
|
||||
style={{display: "inline-block", width: "100%",}}
|
||||
value={mode}
|
||||
onChange={onChangeMode}
|
||||
>
|
||||
@@ -93,39 +133,81 @@ export const ResponseDisplayPlaintext = (props) =>{
|
||||
/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Auto format JSON"} >
|
||||
<IconButton onClick={formatJSON} style={{}}>
|
||||
<CodeIcon color={"info"} style={{cursor: "pointer"}} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={renderColors ? "Render Plaintext" : "Render Colors"} >
|
||||
<IconButton onClick={() => {setRenderColors(!renderColors)}}>
|
||||
<PaletteIcon color={renderColors ? "success" : "secondary"} style={{cursor: "pointer"}} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</div>
|
||||
}
|
||||
{props.displayType !== 'console' &&
|
||||
<div style={{
|
||||
height: "1px",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
zIndex: 1,
|
||||
}}>
|
||||
{showOptions &&
|
||||
<UnfoldLessIcon onClick={onChangeShowOptions}
|
||||
style={{cursor: "pointer", position: "relative", top: "-9px"}}/>
|
||||
}
|
||||
{!showOptions &&
|
||||
<UnfoldMoreIcon onClick={onChangeShowOptions}
|
||||
style={{cursor: "pointer", position: "relative", top: "-9px"}}/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div style={{height: "1px", width: "100%", display: "flex", zIndex: 1, justifyContent: "space-around", backgroundColor: theme.palette.secondary.main}}>
|
||||
{showOptions &&
|
||||
<UnfoldLessIcon onClick={onChangeShowOptions} style={{cursor: "pointer", position: "relative", top: "-8px"}} />
|
||||
}
|
||||
{!showOptions &&
|
||||
<UnfoldMoreIcon onClick={onChangeShowOptions} style={{cursor: "pointer", position: "relative", top: "-7px"}} />
|
||||
}
|
||||
</div>
|
||||
|
||||
<div style={{display: "flex", flexGrow: 1, height: "100%"}}>
|
||||
<AceEditor
|
||||
mode={mode}
|
||||
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
|
||||
fontSize={14}
|
||||
showGutter={true}
|
||||
//onLoad={onLoad}
|
||||
highlightActiveLine={false}
|
||||
showPrintMargin={false}
|
||||
value={plaintextView}
|
||||
height={props.expand ? "100%": undefined}
|
||||
maxLines={props.expand ? undefined : 20}
|
||||
width={"100%"}
|
||||
//autoScrollEditorIntoView={true}
|
||||
wrapEnabled={wrapText}
|
||||
minLines={1}
|
||||
//maxLines={props.expand ? 50 : 20}
|
||||
setOptions={{
|
||||
showLineNumbers: true,
|
||||
tabSize: 4,
|
||||
useWorker: false
|
||||
}}/>
|
||||
<div style={{
|
||||
flexGrow: 1,
|
||||
height: "100%",
|
||||
paddingLeft: renderColors ? "15px" : "0px",
|
||||
paddingRight: renderColors ? "5px" : "0px"
|
||||
}}>
|
||||
{renderColors &&
|
||||
<GetOutputFormatAll data={[
|
||||
{response: plaintextView, id: props?.task?.id || 0, timestamp: "1970-01-01"}]}
|
||||
myTask={false}
|
||||
taskID={props?.task?.id || 0}
|
||||
useASNIColor={true}
|
||||
messagesEndRef={null}
|
||||
showTaskStatus={false}
|
||||
search={undefined}
|
||||
wrapText={wrapText}/>
|
||||
}
|
||||
{!renderColors &&
|
||||
<AceEditor
|
||||
className={"roundedBottomCorners"}
|
||||
ref={currentContentRef}
|
||||
mode={props.displayType !== 'console' ? mode : 'html'}
|
||||
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
|
||||
fontSize={14}
|
||||
showGutter={ props.displayType !== 'console'}
|
||||
onChange={onChangeText}
|
||||
//onLoad={onLoad}
|
||||
highlightActiveLine={false}
|
||||
showPrintMargin={false}
|
||||
value={plaintextView}
|
||||
height={props.expand ? "100%": undefined}
|
||||
maxLines={props.expand ? undefined : 20}
|
||||
width={"100%"}
|
||||
//style={{backgroundColor: "transparent"}}
|
||||
//autoScrollEditorIntoView={true}
|
||||
wrapEnabled={props.displayType !== 'console' ? wrapText : true}
|
||||
minLines={1}
|
||||
setOptions={{
|
||||
showLineNumbers: props.displayType !== 'console',
|
||||
tabSize: 4,
|
||||
useWorker: false,
|
||||
showInvisibles: false,
|
||||
}}/>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -7,12 +7,10 @@ import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
|
||||
export const ResponseDisplayScreenshot = (props) =>{
|
||||
const [openScreenshot, setOpenScreenshot] = React.useState(false);
|
||||
|
||||
const now = (new Date()).toUTCString();
|
||||
const clickOpenScreenshot = () => {
|
||||
setOpenScreenshot(true);
|
||||
}
|
||||
const scrollContent = (node, isAppearing) => {
|
||||
const scrollContent = (node, isAppearing) => {
|
||||
// only auto-scroll if you issued the task
|
||||
document.getElementById(`scrolltotaskbottom${props.task.id}`)?.scrollIntoView({
|
||||
//behavior: "smooth",
|
||||
@@ -20,21 +18,21 @@ export const ResponseDisplayScreenshot = (props) =>{
|
||||
inline: "nearest"
|
||||
})
|
||||
}
|
||||
React.useLayoutEffect( () => {
|
||||
React.useLayoutEffect( () => {
|
||||
scrollContent()
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
{openScreenshot &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openScreenshot}
|
||||
onClose={()=>{setOpenScreenshot(false);}}
|
||||
innerDialog={<ResponseDisplayScreenshotModal images={props.agent_file_id} onClose={()=>{setOpenScreenshot(false);}} />}
|
||||
/>
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openScreenshot}
|
||||
onClose={()=>{setOpenScreenshot(false);}}
|
||||
innerDialog={<ResponseDisplayScreenshotModal images={[props.agent_file_id]} onClose={()=>{setOpenScreenshot(false);}} />}
|
||||
/>
|
||||
}
|
||||
<pre style={{display: "inline-block"}}>
|
||||
{props?.plaintext || ""}
|
||||
</pre>
|
||||
<MythicStyledTooltip title={props?.hoverText || "View Screenshot"} >
|
||||
<MythicStyledTooltip title={props?.hoverText || "View Screenshot (s)"} >
|
||||
<Button color="primary" variant={props.variant ? props.variant : "contained"} onClick={clickOpenScreenshot} style={{marginBottom: "10px"}}>{props.name}</Button>
|
||||
</MythicStyledTooltip>
|
||||
</>
|
||||
|
||||
@@ -18,45 +18,46 @@ export function ResponseDisplayScreenshotModal({onClose, images, startIndex}) {
|
||||
const handleNext = () => {
|
||||
setActiveStep((prevActiveStep) => prevActiveStep + 1);
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
setActiveStep((prevActiveStep) => prevActiveStep - 1);
|
||||
};
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogContent dividers={true} style={{padding: 0}}>
|
||||
<div style={{height: "calc(80vh)", display: zoom ? "" : "flex", alignItems: zoom ? "" : "center" , background: theme.palette.mode === "dark" ? "rgb(44, 52, 60)" : "grey"}}>
|
||||
<img onClick={toggleZoom} src={"/api/v1.4/files/screencaptures/" + images[activeStep]} style={{width: zoom ? "" : "100%", cursor: zoom ? "zoom-out" : "zoom-in", overflow: "auto"}} />
|
||||
< >
|
||||
<DialogContent dividers={true} style={{padding: 0}} >
|
||||
<div style={{height: "calc(80vh)", display: zoom ? "" : "flex", alignItems: zoom ? "" : "flex-start" , background: theme.palette.mode === "dark" ? "rgb(44, 52, 60)" : "grey"}}>
|
||||
<img onClick={toggleZoom} src={"/api/v1.4/files/screencaptures/" + images[activeStep]}
|
||||
style={{width: zoom ? "" : "100%", cursor: zoom ? "zoom-out" : "zoom-in", overflow: "auto"}} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
<MobileStepper
|
||||
variant="text"
|
||||
steps={maxSteps}
|
||||
position="static"
|
||||
activeStep={activeStep}
|
||||
nextButton={
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleNext}
|
||||
disabled={activeStep === maxSteps - 1}
|
||||
>
|
||||
Next
|
||||
{<KeyboardArrowRight />}
|
||||
</Button>
|
||||
}
|
||||
backButton={
|
||||
<Button size="small" onClick={handleBack} disabled={activeStep === 0}>
|
||||
{<KeyboardArrowLeft />}
|
||||
Back
|
||||
</Button>
|
||||
}
|
||||
variant="text"
|
||||
steps={maxSteps}
|
||||
position="static"
|
||||
activeStep={activeStep}
|
||||
nextButton={
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleNext}
|
||||
disabled={activeStep === maxSteps - 1}
|
||||
>
|
||||
Next
|
||||
{<KeyboardArrowRight />}
|
||||
</Button>
|
||||
}
|
||||
backButton={
|
||||
<Button size="small" onClick={handleBack} disabled={activeStep === 0}>
|
||||
{<KeyboardArrowLeft />}
|
||||
Back
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<DialogActions>
|
||||
|
||||
<DialogActions >
|
||||
<Button variant="contained" onClick={onClose} color="primary">
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, {useEffect, useRef} from 'react';
|
||||
import {Button} from '@mui/material';
|
||||
import { MythicViewJSONAsTableDialog, MythicDialog } from '../../MythicComponents/MythicDialog';
|
||||
import {MythicViewJSONAsTableDialog, MythicDialog, MythicModifyStringDialog} from '../../MythicComponents/MythicDialog';
|
||||
import { MythicDisplayTextDialog } from '../../MythicComponents/MythicDisplayTextDialog';
|
||||
import { ResponseDisplayTableDialogTable } from './ResponseDisplayTableDialogTable';
|
||||
import Paper from '@mui/material/Paper';
|
||||
@@ -8,10 +8,6 @@ import Typography from '@mui/material/Typography';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import 'react-virtualized/styles.css';
|
||||
import {TaskFromUIButton} from './TaskFromUIButton';
|
||||
import Grow from '@mui/material/Grow';
|
||||
import Popper from '@mui/material/Popper';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import MenuList from '@mui/material/MenuList';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import { copyStringToClipboard } from '../../utilities/Clipboard';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
@@ -19,7 +15,13 @@ import {snackActions} from '../../utilities/Snackbar';
|
||||
import {MythicStyledTooltip} from '../../MythicComponents/MythicStyledTooltip';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import MythicResizableGrid from '../../MythicComponents/MythicResizableGrid';
|
||||
import {faList, faTrashAlt, faSkullCrossbones, faCamera, faSyringe, faFolder, faFolderOpen, faFileArchive, faCog, faFileWord, faFileExcel, faFilePowerpoint, faFilePdf, faDatabase, faKey, faFileCode, faDownload, faUpload, faFileImage, faCopy, faBoxOpen, faFileAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import {faList, faTrashAlt, faSkullCrossbones, faCamera, faSyringe, faFolder, faFolderOpen, faFileArchive, faCog,
|
||||
faFileWord, faFileExcel, faFilePowerpoint, faFilePdf, faDatabase, faKey, faFileCode, faDownload, faUpload,
|
||||
faFileImage, faCopy, faBoxOpen, faFileAlt, faCirclePlus, faCheck, faSquareXmark, faRotate } from '@fortawesome/free-solid-svg-icons';
|
||||
import {Dropdown, DropdownMenuItem} from "../../MythicComponents/MythicNestedMenus";
|
||||
import {GetComputedFontSize} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {TableFilterDialog} from "./TableFilterDialog";
|
||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||
|
||||
const onCopyToClipboard = (data) => {
|
||||
let result = copyStringToClipboard(data);
|
||||
@@ -31,6 +33,14 @@ const onCopyToClipboard = (data) => {
|
||||
}
|
||||
export const getIconName = (iconName) => {
|
||||
switch(iconName.toLowerCase()){
|
||||
case "add":
|
||||
return faCirclePlus;
|
||||
case "x":
|
||||
return faSquareXmark;
|
||||
case "check":
|
||||
return faCheck;
|
||||
case "refresh":
|
||||
return faRotate;
|
||||
case "openfolder":
|
||||
case "folder":
|
||||
return faFolderOpen;
|
||||
@@ -42,6 +52,7 @@ export const getIconName = (iconName) => {
|
||||
case "diskimage":
|
||||
return faBoxOpen;
|
||||
case "executable":
|
||||
case "cog":
|
||||
return faCog;
|
||||
case "word":
|
||||
return faFileWord;
|
||||
@@ -78,51 +89,82 @@ export const getIconName = (iconName) => {
|
||||
case "camera":
|
||||
return faCamera;
|
||||
default:
|
||||
return faFileAlt;
|
||||
return iconName;
|
||||
}
|
||||
}
|
||||
export const getIconColor = (theme, color) => {
|
||||
switch(color){
|
||||
case "info":
|
||||
return theme.palette.info.main;
|
||||
case "warning":
|
||||
return theme.palette.warning.main;
|
||||
case "primary":
|
||||
return theme.palette.primary.main;
|
||||
case "error":
|
||||
return theme.palette.error.main;
|
||||
case "success":
|
||||
return theme.palette.success.main;
|
||||
case "secondary":
|
||||
return theme.palette.success.main;
|
||||
default:
|
||||
return color;
|
||||
}
|
||||
}
|
||||
const doubleClickRow = () => {
|
||||
|
||||
}
|
||||
const ResponseDisplayTableStringCell = ({cellData, rowData}) => {
|
||||
|
||||
const ResponseDisplayTableStringCellCopy = ({cellData}) => {
|
||||
return (
|
||||
<div style={{...(cellData?.cellStyle || null)}}>
|
||||
{cellData?.copyIcon?
|
||||
<MythicStyledTooltip title={"Copy to clipboard"}>
|
||||
<IconButton onClick={() => onCopyToClipboard(cellData["plaintext"])} size="small">
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip> : null}
|
||||
{cellData?.startIcon?
|
||||
<MythicStyledTooltip title={cellData?.startIconHoverText || ""} >
|
||||
<FontAwesomeIcon icon={getIconName(cellData?.startIcon)} style={{marginRight: "5px", color: cellData?.startIconColor || ""}}/>
|
||||
</MythicStyledTooltip>
|
||||
: null
|
||||
}
|
||||
cellData?.copyIcon &&
|
||||
<MythicStyledTooltip title={"Copy to clipboard"}>
|
||||
<IconButton onClick={() => onCopyToClipboard(cellData["plaintext"])} size="small">
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
)
|
||||
}
|
||||
const ResponseDisplayTableFontAwesomeStartIcon = ({cellData}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
cellData?.startIcon &&
|
||||
<MythicStyledTooltip title={cellData?.startIconHoverText || ""} >
|
||||
<FontAwesomeIcon icon={getIconName(cellData?.startIcon)} style={{marginRight: "5px", color: getIconColor(theme, cellData?.startIconColor || "")}}/>
|
||||
</MythicStyledTooltip>
|
||||
)
|
||||
}
|
||||
const ResponseDisplayTableFontAwesomeEndIcon = ({cellData}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
cellData?.endIcon &&
|
||||
<MythicStyledTooltip title={cellData?.endIconHoverText || ""}>
|
||||
<FontAwesomeIcon icon={getIconName(cellData?.endIcon)} style={{color: getIconColor(theme, cellData?.endIconColor || "")}}/>
|
||||
</MythicStyledTooltip>
|
||||
)
|
||||
}
|
||||
const ResponseDisplayTableStringCell = ({cellData, rowData}) => {
|
||||
return (
|
||||
<div style={{...(cellData?.cellStyle || null), height: "100%"}}>
|
||||
<ResponseDisplayTableStringCellCopy cellData={cellData} />
|
||||
<ResponseDisplayTableFontAwesomeStartIcon cellData={cellData} />
|
||||
{cellData?.plaintextHoverText? (
|
||||
<MythicStyledTooltip title={cellData.plaintextHoverText}>
|
||||
<pre style={{display: "inline-block"}}>
|
||||
<pre style={{display: "inline-block", margin: 0}}>
|
||||
{cellData?.plaintext?.replaceAll?.("\n", "") || " "}
|
||||
</pre>
|
||||
|
||||
</MythicStyledTooltip>
|
||||
) : (
|
||||
<pre style={{display: "inline-block"}}>
|
||||
<pre style={{display: "inline-block", margin: 0}}>
|
||||
{cellData?.plaintext?.replaceAll?.("\n","") || " "}
|
||||
</pre>
|
||||
)}
|
||||
{cellData?.endIcon?
|
||||
<MythicStyledTooltip title={cellData?.endIconHoverText || ""}>
|
||||
<FontAwesomeIcon icon={getIconName(cellData?.endIcon)} style={{color: cellData?.endIconColor || ""}}/>
|
||||
</MythicStyledTooltip>: null
|
||||
}
|
||||
<ResponseDisplayTableFontAwesomeEndIcon cellData={cellData} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const ResponseDisplayTableNumberCell = ({cellData, rowData}) => {
|
||||
return (
|
||||
<div style={{...(cellData?.cellStyle || null)}}>
|
||||
<div style={{...(cellData?.cellStyle || null), height: "100%"}}>
|
||||
{cellData?.copyIcon?
|
||||
<MythicStyledTooltip title={"Copy to clipboard"}>
|
||||
<IconButton onClick={() => onCopyToClipboard(cellData["plaintext"])} size="small">
|
||||
@@ -137,13 +179,13 @@ const ResponseDisplayTableNumberCell = ({cellData, rowData}) => {
|
||||
}
|
||||
{cellData?.plaintextHoverText? (
|
||||
<MythicStyledTooltip title={cellData.plaintextHoverText}>
|
||||
<pre style={{display: "inline-block"}}>
|
||||
<pre style={{display: "inline-block", margin: 0}}>
|
||||
{cellData?.plaintext || " "}
|
||||
</pre>
|
||||
|
||||
</MythicStyledTooltip>
|
||||
) : (
|
||||
<pre style={{display: "inline-block"}}>
|
||||
<pre style={{display: "inline-block", margin: 0}}>
|
||||
{cellData?.plaintext || " "}
|
||||
</pre>
|
||||
)}
|
||||
@@ -179,23 +221,32 @@ export const getStringSize = ({cellData}) => {
|
||||
}
|
||||
const ResponseDisplayTableSizeCell = ({cellData, rowData}) => {
|
||||
return (
|
||||
<div style={{...(cellData?.cellStyle || null)}}>
|
||||
<div style={{...(cellData?.cellStyle || null), height: "100%"}}>
|
||||
{cellData?.plaintextHoverText? (
|
||||
<MythicStyledTooltip title={cellData.plaintextHoverText} >
|
||||
<pre style={{display: "inline-block"}}>
|
||||
<pre style={{display: "inline-block", margin: 0}}>
|
||||
{getStringSize({cellData})}
|
||||
</pre>
|
||||
|
||||
</MythicStyledTooltip>
|
||||
) : (
|
||||
<pre style={{display: "inline-block"}}>
|
||||
<pre style={{display: "inline-block", margin: 0}}>
|
||||
{getStringSize({cellData})}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const actionCellButtonStyle = {paddingTop: 0, paddingBottom: 0};
|
||||
const ResponseDisplayTableActionCell = ({cellData, callback_id, rowData}) => {
|
||||
return (
|
||||
<div style={{ height: "100%"}}>
|
||||
{cellData?.plaintext && cellData.plaintext}
|
||||
{cellData?.button && <ResponseDisplayTableActionCellButton cellData={cellData} callback_id={callback_id} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const ResponseDisplayTableActionCellButton = ({cellData, callback_id}) => {
|
||||
const theme = useTheme();
|
||||
const [openButton, setOpenButton] = React.useState(false);
|
||||
const [openTaskingButton, setOpenTaskingButton] = React.useState(false);
|
||||
@@ -248,191 +299,266 @@ const ResponseDisplayTableActionCell = ({cellData, callback_id, rowData}) => {
|
||||
setOpenTableButton(false);
|
||||
setTaskingData({});
|
||||
}
|
||||
const getButtonObject = () => {
|
||||
switch(cellData.button.type.toLowerCase()){
|
||||
switch(cellData.button.type.toLowerCase()){
|
||||
case "dictionary":
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicStyledTooltip title={cellData?.button?.hoverText || " "} >
|
||||
<Button size="small" variant="contained" color="primary"
|
||||
onClick={() => setOpenButton(true)} disabled={cellData?.button?.disabled || false}
|
||||
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.startIconColor || ""}}/> : null}
|
||||
>{cellData?.button?.name || " "}</Button>
|
||||
</MythicStyledTooltip>
|
||||
{openButton &&
|
||||
<MythicDialog fullWidth={true} maxWidth="lg" open={openButton}
|
||||
onClose={()=>{setOpenButton(false);}}
|
||||
innerDialog={<MythicViewJSONAsTableDialog title={cellData?.button?.title || "Title Here"} leftColumn={cellData?.button?.leftColumnTitle || "Left Column"}
|
||||
rightColumn={cellData?.button?.rightColumnTitle || "Right Column"} value={cellData?.button?.value || {}} onClose={()=>{setOpenButton(false);}} />}
|
||||
/>
|
||||
<React.Fragment>
|
||||
<MythicStyledTooltip title={cellData?.button?.hoverText || "View Data"} >
|
||||
<Button size="small" color="info"
|
||||
onClick={() => setOpenButton(true)} disabled={cellData?.button?.disabled || false}
|
||||
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.disabled ? "unset" : getIconColor(theme, cellData?.button?.startIconColor)}}/> : null}
|
||||
style={{...actionCellButtonStyle}}
|
||||
>{cellData?.button?.name}</Button>
|
||||
</MythicStyledTooltip>
|
||||
{openButton &&
|
||||
<MythicDialog fullWidth={true} maxWidth="lg" open={openButton}
|
||||
onClose={()=>{setOpenButton(false);}}
|
||||
innerDialog={<MythicViewJSONAsTableDialog title={cellData?.button?.title} leftColumn={cellData?.button?.leftColumnTitle}
|
||||
rightColumn={cellData?.button?.rightColumnTitle} value={cellData?.button?.value || {}} onClose={()=>{setOpenButton(false);}} />}
|
||||
/>
|
||||
}
|
||||
</React.Fragment>
|
||||
)
|
||||
case "string":
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicStyledTooltip title={cellData?.button?.hoverText || " "} >
|
||||
<Button size="small" variant="contained" color="primary"
|
||||
onClick={() => setOpenButton(true)} disabled={cellData?.button?.disabled || false}
|
||||
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.startIconColor || ""}}/> : null}
|
||||
>{cellData?.button?.name || " "}</Button>
|
||||
</MythicStyledTooltip>
|
||||
{openButton &&
|
||||
<MythicDisplayTextDialog fullWidth={true} maxWidth="lg" open={openButton} title={cellData?.button?.title || "Title Here"} value={cellData?.button?.value || ""}
|
||||
onClose={()=>{setOpenButton(false);}}
|
||||
/>
|
||||
<React.Fragment>
|
||||
<MythicStyledTooltip title={cellData?.button?.hoverText || "View Data"} >
|
||||
<Button size="small" color="info"
|
||||
onClick={() => setOpenButton(true)} disabled={cellData?.button?.disabled || false}
|
||||
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.disabled ? "unset" : getIconColor(theme, cellData?.button?.startIconColor)}}/> : null}
|
||||
style={{...actionCellButtonStyle}}
|
||||
>{cellData?.button?.name}</Button>
|
||||
</MythicStyledTooltip>
|
||||
{openButton &&
|
||||
<MythicDisplayTextDialog fullWidth={true} maxWidth="lg" open={openButton} title={cellData?.button?.title} value={cellData?.button?.value || ""}
|
||||
onClose={()=>{setOpenButton(false);}}
|
||||
/>
|
||||
}
|
||||
</React.Fragment>
|
||||
</React.Fragment>
|
||||
)
|
||||
case "table":
|
||||
case "table":
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicStyledTooltip title={cellData?.button?.hoverText || " "} >
|
||||
<Button size="small" variant="contained" color="primary"
|
||||
onClick={() => setOpenButton(true)} disabled={cellData?.button?.disabled || false}
|
||||
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.startIconColor || ""}}/> : null}
|
||||
>{cellData?.button?.name || " "}</Button>
|
||||
</MythicStyledTooltip>
|
||||
{openButton &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openButton}
|
||||
onClose={()=>{setOpenButton(false);}}
|
||||
innerDialog={<ResponseDisplayTableDialogTable title={cellData?.button?.title || "Title Here"}
|
||||
table={cellData?.button?.value || {}} callback_id={callback_id} onClose={()=>{setOpenButton(false);}} />}
|
||||
/>
|
||||
<React.Fragment>
|
||||
<MythicStyledTooltip title={cellData?.button?.hoverText || "View Data"} >
|
||||
<Button size="small" color="info"
|
||||
onClick={() => setOpenButton(true)} disabled={cellData?.button?.disabled || false}
|
||||
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.disabled ? "unset" : getIconColor(theme, cellData?.button?.startIconColor || "")}}/> : null}
|
||||
style={{...actionCellButtonStyle}}
|
||||
>{cellData?.button?.name}</Button>
|
||||
</MythicStyledTooltip>
|
||||
{openButton &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openButton}
|
||||
onClose={()=>{setOpenButton(false);}}
|
||||
innerDialog={<ResponseDisplayTableDialogTable title={cellData?.button?.title}
|
||||
table={cellData?.button?.value || {}} callback_id={callback_id} onClose={()=>{setOpenButton(false);}} />}
|
||||
/>
|
||||
}
|
||||
</React.Fragment>
|
||||
</React.Fragment>
|
||||
)
|
||||
case "task":
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MythicStyledTooltip title={cellData?.button?.hoverText || " "}>
|
||||
<Button size="small" onClick={() => setOpenTaskingButton(true)} disabled={cellData?.button?.disabled || false} variant="contained" color="warning"
|
||||
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.startIconColor || ""}}/> : null}
|
||||
>{cellData?.button?.name || " "}</Button>
|
||||
</MythicStyledTooltip>
|
||||
{openTaskingButton &&
|
||||
<TaskFromUIButton ui_feature={cellData?.button?.ui_feature || " "}
|
||||
callback_id={callback_id}
|
||||
parameters={cellData?.button?.parameters || ""}
|
||||
openDialog={cellData?.button?.openDialog || false}
|
||||
getConfirmation={cellData?.button?.getConfirmation || false}
|
||||
acceptText={cellData?.button?.acceptText || "confirm"}
|
||||
selectCallback={cellData?.button?.selectCallback || false}
|
||||
onTasked={() => setOpenTaskingButton(false)}/>
|
||||
}
|
||||
</React.Fragment>
|
||||
<React.Fragment>
|
||||
<MythicStyledTooltip title={cellData?.button?.hoverText || "Submit Task"}>
|
||||
<Button size="small" onClick={() => setOpenTaskingButton(true)} disabled={cellData?.button?.disabled || false} color="warning"
|
||||
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.disabled ? "unset" : getIconColor(theme, cellData?.button?.startIconColor || "")}}/> : undefined}
|
||||
style={{...actionCellButtonStyle}}
|
||||
>{cellData?.button?.name ? cellData?.button?.name : cellData?.button?.startIcon ? undefined : "Submit Task"}</Button>
|
||||
</MythicStyledTooltip>
|
||||
{openTaskingButton &&
|
||||
<TaskFromUIButton ui_feature={cellData?.button?.ui_feature || " "}
|
||||
callback_id={callback_id}
|
||||
parameters={cellData?.button?.parameters || ""}
|
||||
openDialog={cellData?.button?.openDialog || false}
|
||||
getConfirmation={cellData?.button?.getConfirmation || false}
|
||||
acceptText={cellData?.button?.acceptText || "confirm"}
|
||||
selectCallback={cellData?.button?.selectCallback || false}
|
||||
onTasked={() => setOpenTaskingButton(false)}/>
|
||||
}
|
||||
</React.Fragment>
|
||||
)
|
||||
case "menu":
|
||||
return (
|
||||
<React.Fragment>
|
||||
{openTaskingButton &&
|
||||
<TaskFromUIButton ui_feature={taskingData.ui_feature}
|
||||
callback_id={callback_id}
|
||||
parameters={taskingData.parameters}
|
||||
openDialog={taskingData?.openDialog || false}
|
||||
getConfirmation={taskingData?.getConfirmation || false}
|
||||
acceptText={taskingData?.acceptText || "confirm"}
|
||||
selectCallback={taskingData?.selectCallback || false}
|
||||
onTasked={finishedTasking}/>
|
||||
}
|
||||
{openDictionaryButton &&
|
||||
<MythicDialog fullWidth={true} maxWidth="lg" open={openDictionaryButton}
|
||||
onClose={finishedViewingData}
|
||||
innerDialog={<MythicViewJSONAsTableDialog title={taskingData.title} leftColumn={taskingData.leftColumnTitle}
|
||||
rightColumn={taskingData.rightColumnTitle} value={taskingData.value} onClose={finishedViewingData} />}
|
||||
/>
|
||||
}
|
||||
{openStringButton &&
|
||||
<MythicDisplayTextDialog fullWidth={true} maxWidth="lg" open={openStringButton} title={taskingData?.title || "Title Here"} value={taskingData?.value || ""}
|
||||
onClose={finishedViewingData}
|
||||
/>
|
||||
}
|
||||
{openTableButton &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openTableButton}
|
||||
onClose={finishedViewingData}
|
||||
innerDialog={<ResponseDisplayTableDialogTable title={taskingData?.title || "Title Here"}
|
||||
table={taskingData?.value || {}} callback_id={callback_id} onClose={finishedViewingData} />}
|
||||
/>
|
||||
<React.Fragment>
|
||||
{openTaskingButton &&
|
||||
<TaskFromUIButton ui_feature={taskingData.ui_feature}
|
||||
callback_id={callback_id}
|
||||
parameters={taskingData.parameters}
|
||||
openDialog={taskingData?.openDialog || false}
|
||||
getConfirmation={taskingData?.getConfirmation || false}
|
||||
acceptText={taskingData?.acceptText || "confirm"}
|
||||
selectCallback={taskingData?.selectCallback || false}
|
||||
onTasked={finishedTasking}/>
|
||||
}
|
||||
<Button size="small" variant="contained" color="primary" ref={dropdownAnchorRef}
|
||||
onClick={() => setOpenDropdownButton(true)} disabled={cellData?.button?.disabled || false}
|
||||
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.startIconColor || ""}}/> : null}
|
||||
>{cellData?.button?.name || " "}</Button>
|
||||
<Popper open={openDropdownButton} anchorEl={dropdownAnchorRef.current} role={undefined} transition style={{zIndex: 4}}>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
|
||||
}}
|
||||
>
|
||||
<Paper variant="outlined" style={{backgroundColor: theme.palette.mode === 'dark' ? theme.palette.primary.dark : theme.palette.primary.light, color: "white"}}>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<MenuList id="split-button-menu" >
|
||||
{cellData.button.value.map((option, index) => (
|
||||
<MenuItem
|
||||
key={option.name + index}
|
||||
disabled={option.disabled}
|
||||
onClick={(event) => handleMenuItemClick(event, index)}
|
||||
>
|
||||
<MythicStyledTooltip title={option?.hoverText || (option.type === "task" ? "Task an Agent" : "Display Data")}>
|
||||
{option?.startIcon ? <FontAwesomeIcon icon={getIconName(option?.startIcon)} style={{color: option?.startIconColor || "", marginRight: "5px"}}/> : null}
|
||||
{option.name}
|
||||
</MythicStyledTooltip>
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
</React.Fragment>
|
||||
{openDictionaryButton &&
|
||||
<MythicDialog fullWidth={true} maxWidth="lg" open={openDictionaryButton}
|
||||
onClose={finishedViewingData}
|
||||
innerDialog={<MythicViewJSONAsTableDialog title={taskingData.title} leftColumn={taskingData.leftColumnTitle}
|
||||
rightColumn={taskingData.rightColumnTitle} value={taskingData.value} onClose={finishedViewingData} />}
|
||||
/>
|
||||
}
|
||||
{openStringButton &&
|
||||
<MythicDisplayTextDialog fullWidth={true} maxWidth="lg" open={openStringButton} title={taskingData?.title} value={taskingData?.value || ""}
|
||||
onClose={finishedViewingData}
|
||||
/>
|
||||
}
|
||||
{openTableButton &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openTableButton}
|
||||
onClose={finishedViewingData}
|
||||
innerDialog={<ResponseDisplayTableDialogTable title={taskingData?.title}
|
||||
table={taskingData?.value || {}} callback_id={callback_id} onClose={finishedViewingData} />}
|
||||
/>
|
||||
}
|
||||
<Button size="small" ref={dropdownAnchorRef}
|
||||
onClick={() => setOpenDropdownButton(true)} disabled={cellData?.button?.disabled || false}
|
||||
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.disabled ? "unset" : getIconColor(theme, cellData?.button?.startIconColor || "")}}/> : null}
|
||||
style={{...actionCellButtonStyle, color: theme.palette.text.primary}}
|
||||
>{cellData?.button?.name || " "} <ArrowDropDownIcon />
|
||||
</Button>
|
||||
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
|
||||
<Dropdown
|
||||
isOpen={dropdownAnchorRef.current}
|
||||
onOpen={setOpenDropdownButton}
|
||||
externallyOpen={openDropdownButton}
|
||||
menu={
|
||||
cellData.button.value.map((option, index) => (
|
||||
<DropdownMenuItem
|
||||
key={option.name + index}
|
||||
disabled={option.disabled}
|
||||
onClick={(event) => handleMenuItemClick(event, index)}
|
||||
>
|
||||
<MythicStyledTooltip title={option?.hoverText || (option.type === "task" ? "Task an Agent" : "Display Data")}>
|
||||
{option?.startIcon ? <FontAwesomeIcon icon={getIconName(option?.startIcon)} style={{color: getIconColor(theme, cellData?.button?.startIconColor || ""), marginRight: "5px"}}/> : null}
|
||||
{option.name}
|
||||
</MythicStyledTooltip>
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
}
|
||||
/>
|
||||
</ClickAwayListener>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div style={{...(rowData?.rowStyle || null), ...(cellData?.cellStyle || null)}}>
|
||||
{cellData?.plaintext ? cellData.plaintext : null}
|
||||
{cellData?.button ? (getButtonObject()) : (null)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const createRowCells = ({row, rowIndex, headers, callback_id}) => {
|
||||
return headers.map((header, colIndex) => {
|
||||
const cellData = row[header.plaintext];
|
||||
const key = `${rowIndex}-${colIndex}-${header.plaintext}`;
|
||||
switch(header.type){
|
||||
case "string":
|
||||
return <ResponseDisplayTableStringCell key={key} cellData={cellData} rowData={row}/>
|
||||
case "size":
|
||||
return <ResponseDisplayTableSizeCell key={key} cellData={cellData} rowData={row}/>
|
||||
case "button":
|
||||
return <ResponseDisplayTableActionCell callback_id={callback_id} key={key} cellData={cellData} rowData={row}/>
|
||||
case "number":
|
||||
return <ResponseDisplayTableNumberCell callback_id={callback_id} key={key} cellData={cellData} rowData={row} />
|
||||
default:
|
||||
return <ResponseDisplayTableStringCell key={key} cellData={cellData} rowData={row}/>
|
||||
}
|
||||
})
|
||||
}
|
||||
export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
const theme = useTheme();
|
||||
const rowHeight = 35;
|
||||
const headerHeight = 45;
|
||||
const rowHeight = GetComputedFontSize() + 7;
|
||||
const headerHeight = GetComputedFontSize() + 32;
|
||||
const maxHeight = 375;
|
||||
const [dataHeight, setDataHeight] = React.useState(maxHeight);
|
||||
const maxElements = Math.floor(maxHeight / rowHeight);
|
||||
const [allData, setAllData] = React.useState([]);
|
||||
const [selectedColumn, setSelectedColumn] = React.useState({});
|
||||
const [filterOptions, setFilterOptions] = React.useState({});
|
||||
const [openContextMenu, setOpenContextMenu] = React.useState(false);
|
||||
const columns = React.useMemo(
|
||||
() =>
|
||||
table?.headers?.reduce( (prev, cur) => {
|
||||
if(filterOptions[cur.plaintext] && String(filterOptions[cur.plaintext]).length > 0){
|
||||
return [...prev, {...cur, filtered: true}];
|
||||
}else{
|
||||
return [...prev, {...cur}];
|
||||
}
|
||||
}, []) || []
|
||||
, [filterOptions, table.headers]
|
||||
);
|
||||
const [sortData, setSortData] = React.useState({sortKey: null, sortType: null, sortDirection: "ASC"})
|
||||
const filterRow = (row) => {
|
||||
for(const [key,value] of Object.entries(filterOptions)){
|
||||
if(!String(row[key].plaintext).toLowerCase().includes(String(value).toLowerCase())){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const sortedData = React.useMemo(() => {
|
||||
|
||||
const tmpData = [...allData].reduce( (prev, cur) => {
|
||||
if(filterRow(cur)){
|
||||
return [...prev];
|
||||
}else{
|
||||
return [...prev, cur];
|
||||
}
|
||||
}, []);
|
||||
if (sortData.sortKey === null || sortData.sortType === null) {
|
||||
return allData
|
||||
return tmpData
|
||||
}
|
||||
const tmpData = [...allData];
|
||||
if(sortData.sortType === "number" || sortData.sortType === "size"){
|
||||
tmpData.sort((a, b) => (parseInt(a[sortData.sortKey]["plaintext"]) > parseInt(b[sortData.sortKey]["plaintext"]) ? 1 : -1));
|
||||
tmpData.sort((a, b) => {
|
||||
if(a[sortData.sortKey]["plaintext"] === b[sortData.sortKey]["plaintext"]){
|
||||
return 0;
|
||||
}else if(a[sortData.sortKey]["plaintext"] === undefined || a[sortData.sortKey]["plaintext"] === null){
|
||||
return -1;
|
||||
}else if(b[sortData.sortKey]["plaintext"] === undefined || b[sortData.sortKey]["plaintext"] === null){
|
||||
return 1;
|
||||
}else{
|
||||
try{
|
||||
return parseInt(a[sortData.sortKey]["plaintext"]) > parseInt(b[sortData.sortKey]["plaintext"]) ? 1 : -1;
|
||||
}catch(error){
|
||||
console.log("failed to parse ints for sorting", a[sortData.sortKey]["plaintext"], b[sortData.sortKey]["plaintext"]);
|
||||
return a[sortData.sortKey]["plaintext"] > b[sortData.sortKey]["plaintext"] ? 1: -1;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}else if(sortData.sortType === "date"){
|
||||
tmpData.sort((a,b) => ( (new Date(a[sortData.sortKey]["plaintext"])) > (new Date(b[sortData.sortKey]["plaintext"])) ? 1: -1));
|
||||
tmpData.sort((a,b) => {
|
||||
if(a[sortData.sortKey]["plaintext"] === b[sortData.sortKey]["plaintext"]){
|
||||
return 0;
|
||||
}else if(a[sortData.sortKey]["plaintext"] === undefined || a[sortData.sortKey]["plaintext"] === null){
|
||||
return -1;
|
||||
}else if(b[sortData.sortKey]["plaintext"] === undefined || b[sortData.sortKey]["plaintext"] === null){
|
||||
return 1;
|
||||
}else{
|
||||
try{
|
||||
return (new Date(a[sortData.sortKey]["plaintext"])) > (new Date(b[sortData.sortKey]["plaintext"])) ? 1: -1
|
||||
}catch(error){
|
||||
console.log("failed to parse dates for sorting", a[sortData.sortKey]["plaintext"], b[sortData.sortKey]["plaintext"]);
|
||||
return a[sortData.sortKey]["plaintext"] > b[sortData.sortKey]["plaintext"] ? 1: -1;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}else{
|
||||
tmpData.sort( (a, b) => {
|
||||
if(a[sortData.sortKey]["plaintext"] === undefined){
|
||||
if(a[sortData.sortKey]["plaintext"] === b[sortData.sortKey]["plaintext"]){
|
||||
return 0;
|
||||
}else if(a[sortData.sortKey]["plaintext"] === undefined || a[sortData.sortKey]["plaintext"] === null){
|
||||
return -1;
|
||||
}else if(b[sortData.sortKey]["plaintext"] === undefined){
|
||||
}else if(b[sortData.sortKey]["plaintext"] === undefined || b[sortData.sortKey]["plaintext"] === null){
|
||||
return 1;
|
||||
} else {
|
||||
try{
|
||||
return a[sortData.sortKey]["plaintext"].localeCompare(b[sortData.sortKey]["plaintext"]);
|
||||
}catch(error){
|
||||
console.log("failed to localeCompare strings for sorting", a[sortData.sortKey]["plaintext"], b[sortData.sortKey]["plaintext"]);
|
||||
return a[sortData.sortKey]["plaintext"] > b[sortData.sortKey]["plaintext"] ? 1: -1;
|
||||
}
|
||||
}
|
||||
return a[sortData.sortKey]["plaintext"].toLowerCase() > b[sortData.sortKey]["plaintext"].toLowerCase() ? 1 : -1
|
||||
});
|
||||
}
|
||||
if(sortData.sortDirection === "DESC"){
|
||||
tmpData.reverse();
|
||||
}
|
||||
return tmpData;
|
||||
}, [allData, sortData]);
|
||||
}, [allData, sortData, filterOptions]);
|
||||
const onClickHeader = (e, columnIndex) => {
|
||||
const column = table.headers[columnIndex];
|
||||
|
||||
@@ -444,9 +570,9 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
}
|
||||
if (sortData.sortKey === column.plaintext) {
|
||||
if (sortData.sortDirection === 'ASC') {
|
||||
setSortData({sortKey: column.plaintext, sortType: column.type, sortDirection: "DESC"});
|
||||
setSortData({...sortData, sortDirection: "DESC"});
|
||||
} else {
|
||||
setSortData({sortKey: column.plaintext, sortType: column.type, sortDirection: "ASC"});
|
||||
setSortData({sortKey: null, sortType: null, sortDirection: "ASC"});
|
||||
}
|
||||
} else {
|
||||
setSortData({sortKey: column.plaintext, sortType: column.type, sortDirection: "ASC"});
|
||||
@@ -454,41 +580,15 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
};
|
||||
const gridData = React.useMemo(
|
||||
() => {
|
||||
return sortedData.map((row) => {
|
||||
let rowData = [];
|
||||
for(let i = 0; i < table.headers.length; i++){
|
||||
switch(table.headers[i].type){
|
||||
case "string":
|
||||
rowData.push(
|
||||
<ResponseDisplayTableStringCell cellData={row[table.headers[i]['plaintext']]} rowData={row}/>
|
||||
)
|
||||
break;
|
||||
case "size":
|
||||
rowData.push(
|
||||
<ResponseDisplayTableSizeCell cellData={row[table.headers[i]['plaintext']]} rowData={row}/>
|
||||
)
|
||||
break;
|
||||
case "button":
|
||||
rowData.push(
|
||||
<ResponseDisplayTableActionCell callback_id={callback_id} cellData={row[table.headers[i]['plaintext']]} rowData={row}/>
|
||||
)
|
||||
break;
|
||||
case "number":
|
||||
rowData.push(
|
||||
<ResponseDisplayTableNumberCell callback_id={callback_id} cellData={row[table.headers[i]["plaintext"]]} rowData={row} />
|
||||
)
|
||||
break;
|
||||
default:
|
||||
rowData.push(
|
||||
<ResponseDisplayTableStringCell cellData={row[table.headers[i]['plaintext']]} rowData={row}/>
|
||||
)
|
||||
break;
|
||||
}
|
||||
}
|
||||
return [...rowData];
|
||||
return sortedData.map((row, rowIndex) => {
|
||||
return createRowCells({row, headers: table.headers, callback_id, rowIndex});
|
||||
});
|
||||
}, [sortedData]
|
||||
}, [sortedData, table?.headers, callback_id]
|
||||
);
|
||||
const onSubmitFilterOptions = (value) => {
|
||||
setFilterOptions({...filterOptions, [selectedColumn.key]: value });
|
||||
}
|
||||
|
||||
const filterOutButtonsFromRowData = (data) => {
|
||||
let rowData = {};
|
||||
for(const key of Object.keys(data)){
|
||||
@@ -498,16 +598,16 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
}
|
||||
return rowData;
|
||||
}
|
||||
const contextMenuOptions = [
|
||||
const rowContextMenuOptions = [
|
||||
{
|
||||
name: 'Copy Row as JSON',
|
||||
name: 'Copy Row as JSON', icon: null,
|
||||
click: ({event, columnIndex, rowIndex, data}) => {
|
||||
const filteredData = filterOutButtonsFromRowData(data);
|
||||
onCopyToClipboard(JSON.stringify(filteredData, null, 2));
|
||||
}
|
||||
}, type: "item"
|
||||
},
|
||||
{
|
||||
name: 'Copy Row as CSV',
|
||||
name: 'Copy Row as CSV', icon: null,
|
||||
click: ({event, columnIndex, rowIndex, data}) => {
|
||||
const filteredData = filterOutButtonsFromRowData(data);
|
||||
let outputHeaders = "";
|
||||
@@ -525,10 +625,11 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
}
|
||||
}
|
||||
onCopyToClipboard(outputHeaders + "\n" + outputRow);
|
||||
}
|
||||
},
|
||||
type: "item",
|
||||
},
|
||||
{
|
||||
name: 'Copy Row as TSV',
|
||||
{
|
||||
name: 'Copy Row as TSV', icon: null,
|
||||
click: ({event, columnIndex, rowIndex, data}) => {
|
||||
const filteredData = filterOutButtonsFromRowData(data);
|
||||
let outputHeaders = "";
|
||||
@@ -546,43 +647,176 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
}
|
||||
}
|
||||
onCopyToClipboard(outputHeaders + "\n" + outputRow);
|
||||
}
|
||||
},
|
||||
type: "item",
|
||||
},
|
||||
{
|
||||
name: 'Copy Cell', icon: null,
|
||||
click: ({event, columnIndex, rowIndex, data}) => {
|
||||
if(table.headers !== undefined && table.headers[columnIndex] !== undefined){
|
||||
let field = table.headers[columnIndex];
|
||||
if(field.plaintext !== undefined){ // column name
|
||||
if(data[field.plaintext].plaintext !== undefined){
|
||||
onCopyToClipboard(data[field.plaintext].plaintext);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, type: "item"
|
||||
},
|
||||
];
|
||||
const contextMenuOptions = [
|
||||
{
|
||||
name: 'Filter Column', type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if(table.headers[columnIndex].disableFilterMenu){
|
||||
snackActions.warning("Can't filter that column");
|
||||
return;
|
||||
}
|
||||
setSelectedColumn({
|
||||
"key": table.headers[columnIndex].plaintext,
|
||||
"name": table.headers[columnIndex].plaintext
|
||||
});
|
||||
setOpenContextMenu(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Copy Table as JSON", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
let tableData = sortedData.map(d => filterOutButtonsFromRowData(d))
|
||||
onCopyToClipboard(JSON.stringify(tableData, null, 2));
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Copy Table as CSV", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
let tableData = sortedData.map(d => filterOutButtonsFromRowData(d));
|
||||
if(tableData.length === 0){return}
|
||||
let outputHeaders = "";
|
||||
let allOutput = "";
|
||||
|
||||
for(let i = 0; i < tableData.length; i++){
|
||||
let outputRow = "";
|
||||
for(const key of Object.keys(tableData[i])){
|
||||
if(i === 0){
|
||||
if(outputHeaders === ""){
|
||||
outputHeaders += key;
|
||||
} else {
|
||||
outputHeaders += "," + key;
|
||||
}
|
||||
}
|
||||
if(outputRow === ""){
|
||||
outputRow += tableData[i][key];
|
||||
}else{
|
||||
outputRow += "," + tableData[i][key];
|
||||
}
|
||||
}
|
||||
allOutput += outputRow + "\n";
|
||||
}
|
||||
onCopyToClipboard(outputHeaders + "\n" + allOutput);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Copy Table as TSV", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
let tableData = sortedData.map(d => filterOutButtonsFromRowData(d));
|
||||
if(tableData.length === 0){return}
|
||||
let outputHeaders = "";
|
||||
let allOutput = "";
|
||||
|
||||
for(let i = 0; i < tableData.length; i++){
|
||||
let outputRow = "";
|
||||
for(const key of Object.keys(tableData[i])){
|
||||
if(i === 0){
|
||||
if(outputHeaders === ""){
|
||||
outputHeaders += key;
|
||||
} else {
|
||||
outputHeaders += "\t" + key;
|
||||
}
|
||||
}
|
||||
if(outputRow === ""){
|
||||
outputRow += tableData[i][key];
|
||||
}else{
|
||||
outputRow += "\t" + tableData[i][key];
|
||||
}
|
||||
}
|
||||
allOutput += outputRow + "\n";
|
||||
}
|
||||
onCopyToClipboard(outputHeaders + "\n" + allOutput);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Copy Table as PrettyPrint Text", type: "item", icon: null,
|
||||
click: ({event, columnIndex}) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
let tableData = sortedData.map(d => filterOutButtonsFromRowData(d));
|
||||
if(tableData.length === 0){return}
|
||||
let outputHeaders = Object.keys(tableData[0]);
|
||||
let outputHeadersMaxLength = outputHeaders.reduce( (prev, cur) => {
|
||||
return {...prev, [cur]: cur.length}
|
||||
}, {});
|
||||
tableData.map( cur => {
|
||||
for(let i = 0; i < outputHeaders.length; i++){
|
||||
if(outputHeadersMaxLength[outputHeaders[i]] < String(cur[outputHeaders[i]]).length){
|
||||
outputHeadersMaxLength[outputHeaders[i]] = String(cur[outputHeaders[i]]).length;
|
||||
}
|
||||
}
|
||||
});
|
||||
let allOutput = "";
|
||||
for(let i = 0; i < outputHeaders.length; i++){
|
||||
let remainingSpace = Math.max(0, outputHeadersMaxLength[outputHeaders[i]] - outputHeaders[i].length);
|
||||
allOutput += " " + outputHeaders[i].toUpperCase() + " ".repeat(remainingSpace + 2);
|
||||
}
|
||||
allOutput += "\n";
|
||||
for(let i = 0; i < outputHeaders.length; i++){
|
||||
let remainingSpace = Math.max(0, outputHeadersMaxLength[outputHeaders[i]] - outputHeaders[i].length);
|
||||
allOutput += " " + "-".repeat(outputHeaders[i].length) + " ".repeat(remainingSpace + 2);
|
||||
}
|
||||
allOutput += "\n";
|
||||
for(let i = 0; i < tableData.length; i++){
|
||||
for(let j = 0; j < outputHeaders.length; j++){
|
||||
let remainingSpace = Math.max(0, outputHeadersMaxLength[outputHeaders[j]] - String(tableData[i][outputHeaders[j]]).length);
|
||||
allOutput += "| " + String(tableData[i][outputHeaders[j]]) + " ".repeat(remainingSpace) + " ";
|
||||
}
|
||||
allOutput += " |\n"
|
||||
}
|
||||
onCopyToClipboard(allOutput);
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
useEffect( () => {
|
||||
setAllData([...table.rows]);
|
||||
setDataHeight(Math.min(maxHeight, (table.rows.length * rowHeight) + headerHeight));
|
||||
|
||||
}, [table.rows])
|
||||
const sortColumn = table.headers.findIndex((column) => column.plaintext === sortData.sortKey);
|
||||
const tableStyle = React.useMemo( () => {
|
||||
return expand ? {flexGrow: 1, width: "99%",} : {height: dataHeight}
|
||||
}, [expand, dataHeight]);
|
||||
const scrollContent = (node, isAppearing) => {
|
||||
// only auto-scroll if you issued the task
|
||||
document.getElementById(`scrolltotaskbottom${task?.id}`)?.scrollIntoView?.({
|
||||
//behavior: "smooth",
|
||||
block: "end",
|
||||
inline: "nearest"
|
||||
})
|
||||
}
|
||||
React.useLayoutEffect( () => {
|
||||
scrollContent()
|
||||
}, []);
|
||||
return expand ? {flexGrow: 1,
|
||||
minHeight: gridData.length > 0 ? Math.min(maxHeight, dataHeight) : headerHeight,
|
||||
width: "100%", position: "relative"} :
|
||||
{height: dataHeight, position: "relative"}
|
||||
}, [expand, dataHeight, gridData]);
|
||||
return (
|
||||
<div style={{height: "100%", display: "flex", flexDirection: "column", position: "relative", width: "100%"}}>
|
||||
{table?.title ? (
|
||||
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main, marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
|
||||
<Typography variant="h5" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
|
||||
{table?.title && (
|
||||
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main}} variant={"elevation"}>
|
||||
<Typography variant="h6" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
|
||||
{table.title}
|
||||
</Typography>
|
||||
</Paper>
|
||||
) : null}
|
||||
)}
|
||||
|
||||
<div style={tableStyle}>
|
||||
<MythicResizableGrid
|
||||
columns={table.headers}
|
||||
columns={columns}
|
||||
sortIndicatorIndex={sortColumn}
|
||||
sortDirection={sortData.sortDirection}
|
||||
items={gridData}
|
||||
@@ -591,8 +825,24 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
onDoubleClickRow={doubleClickRow}
|
||||
rowHeight={rowHeight}
|
||||
onClickHeader={onClickHeader}
|
||||
rowContextMenuOptions={contextMenuOptions}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
rowContextMenuOptions={rowContextMenuOptions}
|
||||
/>
|
||||
{openContextMenu &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
|
||||
onClose={()=>{setOpenContextMenu(false);}}
|
||||
innerDialog={
|
||||
<MythicModifyStringDialog
|
||||
title='Filter Column'
|
||||
onSubmit={onSubmitFilterOptions}
|
||||
value={filterOptions[selectedColumn.key]}
|
||||
onClose={() => {
|
||||
setOpenContextMenu(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import React from 'react';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import {a11yProps} from "../../MythicComponents/MythicTabPanel";
|
||||
import {ResponseDisplayBrowserScriptComponent} from "./ResponseDisplay";
|
||||
|
||||
function ResponseDisplayTabsLabel(props) {
|
||||
const { label, index, ...other } =
|
||||
props;
|
||||
return (
|
||||
<Tab
|
||||
label={
|
||||
<span>
|
||||
{label}
|
||||
</span>
|
||||
}
|
||||
sx={{
|
||||
'&.Mui-selected': {
|
||||
border: "2px solid grey",
|
||||
borderRadius: "4px",
|
||||
//backgroundColor: 'info.main', // Color when selected
|
||||
},
|
||||
}}
|
||||
wrapped={true}
|
||||
{...a11yProps(index)}
|
||||
{...other}
|
||||
/>
|
||||
);
|
||||
}
|
||||
function ResponseDisplayTabsPanel(props) {
|
||||
const { children, value, index, ...other } =
|
||||
props;
|
||||
const style =
|
||||
props.style === undefined
|
||||
? {
|
||||
display: value === index ? 'flex' : 'none',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
overflowY: "auto",
|
||||
}
|
||||
: props.style;
|
||||
return (
|
||||
<div
|
||||
role='tabpanel'
|
||||
hidden={value !== index}
|
||||
id={`scrollable-auto-tabpanel-${index}`}
|
||||
aria-labelledby={`scrollable-auto-tab-${index}`}
|
||||
style={style}
|
||||
{...other}>
|
||||
{<React.Fragment>{children}</React.Fragment>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export function ResponseDisplayTabs({ tabs, task, expand, displayType, output }) {
|
||||
const mountedRef = React.useRef(true);
|
||||
const [value, setValue] = React.useState(0);
|
||||
const handleChange = (event, newValue) => {
|
||||
setValue(newValue);
|
||||
};
|
||||
React.useEffect( () => {
|
||||
return() => {
|
||||
mountedRef.current = false;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div style={{width: "100%", maxWidth: "100%", display: 'flex', flexDirection: 'column', flexGrow: 1, height: expand ? "100%" : "400px", }}>
|
||||
<AppBar color='default' position={"static"} style={{maxHeight: "25%", overflowY: 'auto', maxWidth: "100%", flexWrap: "no-wrap"}} >
|
||||
<Tabs
|
||||
value={value}
|
||||
variant={"standard"}
|
||||
onChange={handleChange}
|
||||
indicatorColor='primary'
|
||||
textColor='primary'
|
||||
style={{ }}
|
||||
TabIndicatorProps={{style: {
|
||||
display: "none",
|
||||
}}}
|
||||
aria-label='scrollable tabs'>
|
||||
{tabs.map((tab, index) => (
|
||||
<ResponseDisplayTabsLabel
|
||||
key={'tablabel' + task.id + index}
|
||||
index={index}
|
||||
label={tab.title}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</AppBar>
|
||||
{tabs.map((tab, index) => (
|
||||
<ResponseDisplayTabsPanel
|
||||
key={'tabpanel' + task.id + index}
|
||||
value={value}
|
||||
index={index}>
|
||||
<ResponseDisplayBrowserScriptComponent
|
||||
task={task} expand={expand} displayType={displayType} output={output}
|
||||
browserScriptData={tab.content}
|
||||
/>
|
||||
</ResponseDisplayTabsPanel>
|
||||
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,8 +24,9 @@ export function TableFilterDialog({filterOptions, onSubmit, onClose, selectedCol
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Filter {selectedColumn.name} Entries</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
<DialogTitle id="form-dialog-title">Filter "{selectedColumn.name}" Entries</DialogTitle>
|
||||
<DialogContent style={{paddingBottom: 0}} dividers={true}>
|
||||
Show only rows that include the following case insensitive text:
|
||||
<MythicTextField autoFocus onChange={onChange} value={description} onEnter={onCommitSubmit}/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import React, {useState} from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import MythicTextField from '../../MythicComponents/MythicTextField';
|
||||
import {useQuery, gql, useMutation} from '@apollo/client';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import {MythicModifyStringDialog} from "../../MythicComponents/MythicDialog";
|
||||
|
||||
const updateCommentMutation = gql`
|
||||
mutation updateComment ($task_id: Int!, $comment: String) {
|
||||
@@ -42,6 +38,7 @@ export function TaskCommentDialog(props) {
|
||||
const [updateComment] = useMutation(updateCommentMutation, {
|
||||
update: (cache, {data}) => {
|
||||
//console.log(data);
|
||||
props.onClose();
|
||||
}
|
||||
});
|
||||
if (loading) {
|
||||
@@ -51,28 +48,17 @@ export function TaskCommentDialog(props) {
|
||||
console.error(error);
|
||||
return <div>Error!</div>;
|
||||
}
|
||||
const onCommitSubmit = () => {
|
||||
updateComment({variables: {task_id: props.task_id, comment: comment}});
|
||||
props.onClose();
|
||||
const onCommitSubmit = (newValue) => {
|
||||
updateComment({variables: {task_id: props.task_id, comment: newValue}});
|
||||
}
|
||||
const onChange = (name, value, error) => {
|
||||
setComment(value);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Edit Task Comment</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
<MythicTextField autoFocus onEnter={onCommitSubmit} onChange={onChange} value={comment} />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
Close
|
||||
</Button>
|
||||
<Button onClick={onCommitSubmit} variant="contained" color="success">
|
||||
Submit
|
||||
</Button>
|
||||
</DialogActions>
|
||||
<MythicModifyStringDialog title={`Edit Task Comment`}
|
||||
onClose={props.onClose}
|
||||
value={comment}
|
||||
dontCloseOnSubmit={true}
|
||||
onSubmit={onCommitSubmit} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -27,7 +27,6 @@ import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons';
|
||||
import { faExternalLinkAlt, faExpandArrowsAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import SpeedDial from '@mui/material/SpeedDial';
|
||||
import SpeedDialIcon from '@mui/material/SpeedDialIcon';
|
||||
import SpeedDialAction from '@mui/material/SpeedDialAction';
|
||||
import { Backdrop } from '@mui/material';
|
||||
import {downloadFileFromMemory} from '../../utilities/Clipboard';
|
||||
@@ -36,6 +35,12 @@ import html2canvas from 'html2canvas';
|
||||
import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen';
|
||||
import CodeOffIcon from '@mui/icons-material/CodeOff';
|
||||
import SettingsTwoToneIcon from '@mui/icons-material/SettingsTwoTone';
|
||||
import {b64DecodeUnicode} from './ResponseDisplay';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {EventTriggerContextSelectDialog} from "../Eventing/EventTriggerContextSelect";
|
||||
import PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwoTone';
|
||||
|
||||
const ReissueTaskMutationGQL = gql`
|
||||
mutation reissueTaskMutation($task_id: Int!){
|
||||
@@ -69,7 +74,7 @@ export const TaskDisplayContainer = ({task, me}) => {
|
||||
const responseRef = React.useRef(null);
|
||||
useEffect( () => {
|
||||
setCommandID(task.command === null ? 0 : task.command.id);
|
||||
}, [task.command]);
|
||||
}, [task.command?.id]);
|
||||
const toggleViewBrowserScript = React.useCallback( () => {
|
||||
setViewBrowserScript(!viewBrowserScript);
|
||||
}, [viewBrowserScript]);
|
||||
@@ -90,11 +95,12 @@ export const TaskDisplayContainer = ({task, me}) => {
|
||||
me={me}
|
||||
responseRef={responseRef}
|
||||
viewBrowserScript={viewBrowserScript}
|
||||
style={{position: "relative", zIndex: 2, display: "flex", flexDirection: "row-reverse", width: '100%', bottom: "20px", right: "5px", height: 0}}
|
||||
style={{position: "relative", zIndex: 2, display: "flex", flexDirection: "row-reverse", width: '100%',
|
||||
bottom: "25px", right: "4px", height: 0}}
|
||||
fabStyle={{ }}
|
||||
viewAllOutput={selectAllOutput}/>
|
||||
<Grid item xs={12}>
|
||||
<ResponseDisplay
|
||||
<Grid size={12}>
|
||||
<ResponseDisplay
|
||||
task={task}
|
||||
me={me}
|
||||
command_id={commandID}
|
||||
@@ -118,7 +124,7 @@ export const TaskDisplayContainerFlat = ({task, me}) => {
|
||||
setSearchOutput(false);
|
||||
setSelectAllOutput(false);
|
||||
setViewBrowserScript(true);
|
||||
}, [task.command, task.id]);
|
||||
}, [task.command?.id, task.id]);
|
||||
const toggleViewBrowserScript = React.useCallback( () => {
|
||||
setViewBrowserScript(!viewBrowserScript);
|
||||
}, [viewBrowserScript]);
|
||||
@@ -130,8 +136,9 @@ export const TaskDisplayContainerFlat = ({task, me}) => {
|
||||
}, [searchOutput]);
|
||||
|
||||
return (
|
||||
<div style={{ height: "100%", position: "relative", display: "flex", flexDirection: "column", overflowY: "auto", }}>
|
||||
<div style={{ height: "100%", position: "relative", display: "flex", overflowY: "auto", justifyContent: "flex-end" }}>
|
||||
<ResponseDisplay
|
||||
key={task.id}
|
||||
task={task}
|
||||
me={me}
|
||||
command_id={commandID}
|
||||
@@ -141,13 +148,13 @@ export const TaskDisplayContainerFlat = ({task, me}) => {
|
||||
expand={true}
|
||||
responseRef={responseRef}
|
||||
/>
|
||||
<SpeedDialDisplayGeneric toggleViewBrowserScript={toggleViewBrowserScript}
|
||||
<SideDisplayGeneric toggleViewBrowserScript={toggleViewBrowserScript}
|
||||
toggleSelectAllOutput={toggleSelectAllOutput}
|
||||
toggleOpenSearch={toggleOpenSearch}
|
||||
taskData={task}
|
||||
viewBrowserScript={viewBrowserScript}
|
||||
me={me} responseRef={responseRef}
|
||||
style={{position: "absolute", bottom: 0, zIndex: 2}}
|
||||
style={{position: "absolute", bottom: "15px", zIndex: 2}}
|
||||
fabStyle={{ }}
|
||||
viewAllOutput={selectAllOutput}/>
|
||||
</div>
|
||||
@@ -156,35 +163,15 @@ export const TaskDisplayContainerFlat = ({task, me}) => {
|
||||
}
|
||||
export const TaskDisplayContainerConsole = ({task, me}) => {
|
||||
const [viewBrowserScript, setViewBrowserScript] = React.useState(true);
|
||||
const [commandID, setCommandID] = React.useState(0);
|
||||
const [commandID, setCommandID] = React.useState(task?.command?.id || 0);
|
||||
const [searchOutput, setSearchOutput] = React.useState(false);
|
||||
const [selectAllOutput, setSelectAllOutput] = React.useState(false);
|
||||
const responseRef = React.useRef(null);
|
||||
useEffect( () => {
|
||||
setCommandID(task.command === null ? 0 : task.command.id);
|
||||
}, [task.command]);
|
||||
const toggleViewBrowserScript = React.useCallback( () => {
|
||||
setViewBrowserScript(!viewBrowserScript);
|
||||
}, [viewBrowserScript]);
|
||||
const toggleSelectAllOutput = React.useCallback( () => {
|
||||
setSelectAllOutput(!selectAllOutput);
|
||||
}, [selectAllOutput]);
|
||||
const toggleOpenSearch = React.useCallback( () => {
|
||||
setSearchOutput(!searchOutput);
|
||||
}, [searchOutput]);
|
||||
}, [task.command?.id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SpeedDialDisplayGeneric toggleViewBrowserScript={toggleViewBrowserScript}
|
||||
toggleSelectAllOutput={toggleSelectAllOutput}
|
||||
toggleOpenSearch={toggleOpenSearch}
|
||||
taskData={task}
|
||||
me={me}
|
||||
viewBrowserScript={viewBrowserScript}
|
||||
responseRef={responseRef}
|
||||
style={{ bottom: "20px", right: "5px", zIndex: 2, height: "0px", position: "relative", display: "flex", flexDirection: "row-reverse"}}
|
||||
fabStyle={{ }}
|
||||
viewAllOutput={true}/>
|
||||
<ResponseDisplayConsole
|
||||
task={task}
|
||||
me={me}
|
||||
@@ -197,23 +184,278 @@ export const TaskDisplayContainerConsole = ({task, me}) => {
|
||||
);
|
||||
}
|
||||
|
||||
// the base64 decode function to handle unicode was pulled from the following stack overflow post
|
||||
// https://stackoverflow.com/a/30106551
|
||||
function b64DecodeUnicode(str) {
|
||||
// Going backwards: from bytestream, to percent-encoding, to original string.
|
||||
//console.log("decoding", str);
|
||||
try{
|
||||
return decodeURIComponent(window.atob(str));
|
||||
}catch(error){
|
||||
//console.log("Failed to base64 decode response", error)
|
||||
return atob(str);
|
||||
}
|
||||
const SideDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput,
|
||||
toggleOpenSearch, taskData, viewAllOutput, me,
|
||||
responseRef, viewBrowserScript}) => {
|
||||
const theme = useTheme();
|
||||
const [task, setTask] = React.useState(taskData || {});
|
||||
const eventingDataRef = React.useRef({name: "", value: 0});
|
||||
const [openEventingDialog, setOpenEventingDialog] = React.useState(false);
|
||||
const [openTaskTagDialog, setOpenTaskTagDialog] = React.useState(false);
|
||||
const [openCommentDialog, setOpenCommentDialog] = React.useState(false);
|
||||
const [openParametersDialog, setOpenParametersDialog] = React.useState(false);
|
||||
const [openTokenDialog, setOpenTokenDialog] = React.useState(false);
|
||||
const [openStdoutStderrDialog, setOpenStdoutStderrDialog] = React.useState(false);
|
||||
const [openOpsecDialog, setOpenOpsecDialog] = React.useState({open: false, view: "pre"});
|
||||
const [downloadResponses] = useLazyQuery(getAllResponsesLazyQuery, {
|
||||
fetchPolicy: "network-only",
|
||||
onCompleted: (data) => {
|
||||
const output = data.response.reduce( (prev, cur) => {
|
||||
return prev + b64DecodeUnicode(cur.response);
|
||||
}, b64DecodeUnicode(""));
|
||||
downloadFileFromMemory(output, "task_" + task.id + ".txt");
|
||||
},
|
||||
onError: (data) => {
|
||||
|
||||
}
|
||||
});
|
||||
React.useEffect( () => {
|
||||
setTask(taskData);
|
||||
}, [taskData.id, taskData.token, taskData.original_params, taskData.opsec_pre_blocked, taskData.opsec_pre_bypassed, taskData.opsec_post_blocked, taskData.opsec_post_bypassed])
|
||||
const onDownloadResponses = () => {
|
||||
downloadResponses({variables: {task_id: task.id}});
|
||||
};
|
||||
const copyToClipboard = () => {
|
||||
let command = task?.command?.cmd || task.command_name;
|
||||
let result = copyStringToClipboard(command + " " + task.original_params);
|
||||
if(result){
|
||||
snackActions.success("Copied text!");
|
||||
}else{
|
||||
snackActions.error("Failed to copy text");
|
||||
}
|
||||
};
|
||||
const [reissueTask] = useMutation(ReissueTaskMutationGQL, {
|
||||
onCompleted: data => {
|
||||
if(data.reissue_task.status === "success"){
|
||||
snackActions.success("Successfully re-issued task to Mythic");
|
||||
}else{
|
||||
snackActions.error("Failed to re-issue task to Mythic: " + data.reissue_task.error);
|
||||
}
|
||||
},
|
||||
onError: data => {
|
||||
console.log(data);
|
||||
snackActions.error("Failed to re-issue task: " + data);
|
||||
}
|
||||
});
|
||||
const [reissueTaskHandler] = useMutation(ReissueTaskHandlerMutationGQL, {
|
||||
onCompleted: data => {
|
||||
if(data.reissue_task_handler.status === "success"){
|
||||
snackActions.success("Successfully resubmitted task for handling");
|
||||
}else{
|
||||
snackActions.warning("Failed to resubmit task for handling: " + data.reissue_task_handler.error);
|
||||
}
|
||||
|
||||
},
|
||||
onError: data => {
|
||||
console.log(data);
|
||||
snackActions.error("Error resubmitting task for handling: " + data);
|
||||
}
|
||||
});
|
||||
const onDownloadImageClickPng = () => {
|
||||
// we calculate a transform for the nodes so that all nodes are visible
|
||||
// we then overwrite the transform of the `.react-flow__viewport` element
|
||||
// with the style option of the html-to-image library
|
||||
snackActions.info("Saving image to png...");
|
||||
(async () => {
|
||||
const canvas = await html2canvas(responseRef.current);
|
||||
const image = canvas.toDataURL("image/png", 1.0);
|
||||
const fakeLink = window.document.createElement("a");
|
||||
fakeLink.style = "display:none;";
|
||||
fakeLink.download = "task_output.png";
|
||||
|
||||
fakeLink.href = image;
|
||||
|
||||
document.body.appendChild(fakeLink);
|
||||
fakeLink.click();
|
||||
document.body.removeChild(fakeLink);
|
||||
|
||||
fakeLink.remove();
|
||||
|
||||
})();
|
||||
};
|
||||
const onReissueTask = () => {
|
||||
reissueTask({variables: {task_id: task.id}});
|
||||
}
|
||||
const onReissueTaskHandler = () => {
|
||||
reissueTaskHandler({variables: {task_id: task.id}});
|
||||
}
|
||||
const onTriggerEventing = () => {
|
||||
eventingDataRef.current = {
|
||||
name: "task_id",
|
||||
value: task.id
|
||||
};
|
||||
setOpenEventingDialog(true);
|
||||
}
|
||||
return (
|
||||
<div style={{height: "100%"}}>
|
||||
{openTaskTagDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openTaskTagDialog}
|
||||
onClose={()=>{setOpenTaskTagDialog(false);}}
|
||||
innerDialog={<ViewEditTagsDialog me={me} target_object={"task_id"} target_object_id={task.id} onClose={()=>{setOpenTaskTagDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
{openCommentDialog && <MythicDialog fullWidth={true} maxWidth="md" open={openCommentDialog}
|
||||
onClose={()=>{setOpenCommentDialog(false);}}
|
||||
innerDialog={<TaskCommentDialog task_id={task.id} onClose={()=>{setOpenCommentDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
{openParametersDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openParametersDialog}
|
||||
onClose={()=>{setOpenParametersDialog(false);}}
|
||||
innerDialog={<TaskViewParametersDialog task_id={task.id} onClose={()=>{setOpenParametersDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
{openTokenDialog && <MythicDialog fullWidth={true} maxWidth="md" open={openTokenDialog}
|
||||
onClose={()=>{setOpenTokenDialog(false);}}
|
||||
innerDialog={<TaskTokenDialog token_id={task.token === undefined ? 0 : task.token.id} onClose={()=>{setOpenTokenDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
{openOpsecDialog.open && <MythicDialog fullWidth={true} maxWidth="lg" open={openOpsecDialog.open}
|
||||
onClose={()=>{setOpenOpsecDialog({...openOpsecDialog, open: false});}}
|
||||
innerDialog={<TaskOpsecDialog task_id={task.id} view={openOpsecDialog.view} onClose={()=>{setOpenOpsecDialog({...openOpsecDialog, open: false});}} />}
|
||||
/>
|
||||
}
|
||||
|
||||
{openStdoutStderrDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openStdoutStderrDialog}
|
||||
onClose={()=>{setOpenStdoutStderrDialog(false);}}
|
||||
innerDialog={<TaskViewStdoutStderrDialog task_id={task.id} onClose={()=>{setOpenStdoutStderrDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
{openEventingDialog &&
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
maxWidth="xl"
|
||||
open={openEventingDialog}
|
||||
onClose={() => {setOpenEventingDialog(false);}}
|
||||
innerDialog={
|
||||
<EventTriggerContextSelectDialog onClose={() => {setOpenEventingDialog(false);}}
|
||||
triggerContext={eventingDataRef.current}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
<Paper elevation={5} style={{width: "30px", display: "flex", flexDirection: "column", alignItems: "center", overflow: "hidden",
|
||||
backgroundColor: "transparent", height: "100%"}}>
|
||||
<MythicStyledTooltip title={"Toggle BrowserScript"} >
|
||||
<IconButton onClick={toggleViewBrowserScript} style={{paddingBottom: "3px"}}>
|
||||
{viewBrowserScript ? <CodeOffIcon color={"error"} /> : <CodeIcon color={"success"}/>}
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={viewAllOutput ? "View Paginated Output" : "View All Output"}>
|
||||
<IconButton onClick={toggleSelectAllOutput} style={{paddingBottom: "3px"}}>
|
||||
{viewAllOutput ? <CloseFullscreenIcon color={"error"} /> : <FontAwesomeIcon style={{color: theme.palette.success.main}} icon={faExpandArrowsAlt} size="sm" />}
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Search Output"}>
|
||||
<IconButton onClick={toggleOpenSearch} style={{paddingBottom: "3px"}}>
|
||||
<SearchIcon color={"info"} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Download output"}>
|
||||
<IconButton onClick={onDownloadResponses} style={{paddingBottom: "3px"}}>
|
||||
<GetAppIcon color={"success"}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Download screenshot of output"}>
|
||||
<IconButton onClick={onDownloadImageClickPng} style={{paddingBottom: "3px"}}>
|
||||
<InsertPhotoIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Edit Tags"}>
|
||||
<IconButton onClick={()=>{setOpenTaskTagDialog(true)}} style={{paddingBottom: "3px"}}>
|
||||
<LocalOfferOutlinedIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Open Task in New Window"}>
|
||||
<IconButton onClick={()=> {window.open('/new/task/' + task.display_id, "_blank")}} style={{paddingBottom: "3px"}}>
|
||||
<FontAwesomeIcon icon={faExternalLinkAlt} size="sm" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Copy original params to clipboard"}>
|
||||
<IconButton onClick={copyToClipboard} style={{paddingBottom: "3px"}}>
|
||||
<FileCopyOutlinedIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Edit Comment"}>
|
||||
<IconButton onClick={()=>{setOpenCommentDialog(true)}} style={{paddingBottom: "3px"}}>
|
||||
<RateReviewOutlinedIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"View All Parameters And Timestamps"}>
|
||||
<IconButton onClick={()=>{setOpenParametersDialog(true);}} style={{paddingBottom: "3px"}}>
|
||||
<KeyboardIcon/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"View Stdout/Stderr of Task"}>
|
||||
<IconButton onClick={()=>{setOpenStdoutStderrDialog(true);}} style={{paddingBottom: "3px"}}>
|
||||
<FontAwesomeIcon style={{color: theme.palette.error.main}} icon={faExclamationTriangle} size="sm" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Trigger Eventing Based on Task"}>
|
||||
<IconButton onClick={()=>{onTriggerEventing();}} style={{paddingBottom: "3px"}}>
|
||||
<PlayCircleFilledTwoToneIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
{task.opsec_pre_blocked === null ? null : ( task.opsec_pre_bypassed === false ? (
|
||||
<MythicStyledTooltip title={"Submit OPSEC PreCheck Bypass Request"}>
|
||||
<IconButton onClick={()=>{setOpenOpsecDialog({open: true, view: "pre"})}} style={{paddingBottom: "3px"}}>
|
||||
<LockIcon style={{color: theme.palette.error.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
): (
|
||||
<MythicStyledTooltip title={"View OPSEC PreCheck Data"}>
|
||||
<IconButton onClick={()=>{setOpenOpsecDialog({open: true, view: "pre"})}} style={{paddingBottom: "3px"}}>
|
||||
<LockOpenIcon style={{color: theme.palette.success.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
)
|
||||
)
|
||||
}
|
||||
{task.opsec_post_blocked === null ? null : ( task.opsec_post_bypassed === false ? (
|
||||
<MythicStyledTooltip title={"Submit OPSEC PostCheck Bypass Request"}>
|
||||
<IconButton onClick={()=>{setOpenOpsecDialog({open: true, view: "post"})}} style={{paddingBottom: "3px"}}>
|
||||
<LockIcon style={{color: theme.palette.error.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
): (
|
||||
<MythicStyledTooltip title={"View OPSEC PostCheck Data"}>
|
||||
<IconButton onClick={()=>{setOpenOpsecDialog({open: true, view: "post"})}} style={{paddingBottom: "3px"}}>
|
||||
<LockOpenIcon style={{color: theme.palette.success.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
)
|
||||
)
|
||||
}
|
||||
{task.token === null ? null : (
|
||||
<MythicStyledTooltip title={"View Token Information"}>
|
||||
<IconButton onClick={()=>{setOpenTokenDialog(true)}} style={{paddingBottom: "3px"}}>
|
||||
<ConfirmationNumberIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
)}
|
||||
{task.status.toLowerCase().includes("error: container") &&
|
||||
<MythicStyledTooltip title={"Resubmit Tasking"}>
|
||||
<IconButton onClick={onReissueTask} style={{paddingBottom: "3px"}}>
|
||||
<ReplayIcon style={{color: theme.palette.warning.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
{task.status.toLowerCase().includes("error: task") &&
|
||||
<MythicStyledTooltip title={"Resubmit Task Handler"}>
|
||||
<IconButton onClick={onReissueTaskHandler} style={{paddingBottom: "3px"}}>
|
||||
<ReplayIcon style={{color: theme.palette.warning.main}}/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
</Paper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput,
|
||||
toggleOpenSearch, taskData, viewAllOutput, me,
|
||||
responseRef, style, fabStyle, viewBrowserScript}) => {
|
||||
const theme = useTheme();
|
||||
const [task, setTask] = React.useState(taskData || {});
|
||||
const eventingDataRef = React.useRef({name: "", value: 0});
|
||||
const [openEventingDialog, setOpenEventingDialog] = React.useState(false);
|
||||
const [openSpeedDial, setOpenSpeedDial] = React.useState(false);
|
||||
const [openTaskTagDialog, setOpenTaskTagDialog] = React.useState(false);
|
||||
const [openCommentDialog, setOpenCommentDialog] = React.useState(false);
|
||||
@@ -241,7 +483,8 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput
|
||||
setOpenSpeedDial(false);
|
||||
};
|
||||
const copyToClipboard = () => {
|
||||
let result = copyStringToClipboard(task.original_params);
|
||||
let command = task?.command?.cmd || task.command_name;
|
||||
let result = copyStringToClipboard(command + " " + task.original_params);
|
||||
if(result){
|
||||
snackActions.success("Copied text!");
|
||||
}else{
|
||||
@@ -304,45 +547,60 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput
|
||||
const onReissueTaskHandler = () => {
|
||||
reissueTaskHandler({variables: {task_id: task.id}});
|
||||
}
|
||||
const onTriggerEventing = () => {
|
||||
eventingDataRef.current = {
|
||||
name: "task_id",
|
||||
value: task.id
|
||||
};
|
||||
setOpenEventingDialog(true);
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Backdrop open={openSpeedDial} onClick={()=>{setOpenSpeedDial(false);}} style={{zIndex: 2, position: "absolute"}}/>
|
||||
{openTaskTagDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="md" open={openTaskTagDialog}
|
||||
{openTaskTagDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openTaskTagDialog}
|
||||
onClose={()=>{setOpenTaskTagDialog(false);}}
|
||||
innerDialog={<ViewEditTagsDialog me={me} target_object={"task_id"} target_object_id={task.id} onClose={()=>{setOpenTaskTagDialog(false);}} />}
|
||||
/>) : null}
|
||||
{openCommentDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="md" open={openCommentDialog}
|
||||
/>
|
||||
}
|
||||
{openCommentDialog && <MythicDialog fullWidth={true} maxWidth="md" open={openCommentDialog}
|
||||
onClose={()=>{setOpenCommentDialog(false);}}
|
||||
innerDialog={<TaskCommentDialog task_id={task.id} onClose={()=>{setOpenCommentDialog(false);}} />}
|
||||
/>) : null
|
||||
/>
|
||||
}
|
||||
{openParametersDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="md" open={openParametersDialog}
|
||||
{openParametersDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openParametersDialog}
|
||||
onClose={()=>{setOpenParametersDialog(false);}}
|
||||
innerDialog={<TaskViewParametersDialog task_id={task.id} onClose={()=>{setOpenParametersDialog(false);}} />}
|
||||
/>) : null
|
||||
/>
|
||||
}
|
||||
{openTokenDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="md" open={openTokenDialog}
|
||||
{openTokenDialog && <MythicDialog fullWidth={true} maxWidth="md" open={openTokenDialog}
|
||||
onClose={()=>{setOpenTokenDialog(false);}}
|
||||
innerDialog={<TaskTokenDialog token_id={task.token === undefined ? 0 : task.token.id} onClose={()=>{setOpenTokenDialog(false);}} />}
|
||||
/>) : null
|
||||
/>
|
||||
}
|
||||
{openOpsecDialog.open ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="md" open={openOpsecDialog.open}
|
||||
{openOpsecDialog.open && <MythicDialog fullWidth={true} maxWidth="lg" open={openOpsecDialog.open}
|
||||
onClose={()=>{setOpenOpsecDialog({...openOpsecDialog, open: false});}}
|
||||
innerDialog={<TaskOpsecDialog task_id={task.id} view={openOpsecDialog.view} onClose={()=>{setOpenOpsecDialog({...openOpsecDialog, open: false});}} />}
|
||||
/>) : null
|
||||
/>
|
||||
}
|
||||
|
||||
{openStdoutStderrDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="md" open={openStdoutStderrDialog}
|
||||
{openStdoutStderrDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openStdoutStderrDialog}
|
||||
onClose={()=>{setOpenStdoutStderrDialog(false);}}
|
||||
innerDialog={<TaskViewStdoutStderrDialog task_id={task.id} onClose={()=>{setOpenStdoutStderrDialog(false);}} />}
|
||||
/>) : null
|
||||
/>
|
||||
}
|
||||
{openEventingDialog &&
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
maxWidth="xl"
|
||||
open={openEventingDialog}
|
||||
onClose={() => {setOpenEventingDialog(false);}}
|
||||
innerDialog={
|
||||
<EventTriggerContextSelectDialog onClose={() => {setOpenEventingDialog(false);}}
|
||||
triggerContext={eventingDataRef.current}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
<SpeedDial
|
||||
ariaLabel="Task Speeddial"
|
||||
icon={<SettingsTwoToneIcon fontSize={"large"} />}
|
||||
@@ -419,7 +677,7 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput
|
||||
icon={<KeyboardIcon/>}
|
||||
arrow
|
||||
tooltipPlacement={"top"}
|
||||
tooltipTitle={"View All Parameters"}
|
||||
tooltipTitle={"View All Parameters And Timestamps"}
|
||||
onClick={()=>{setOpenParametersDialog(true);setOpenSpeedDial(false);}}
|
||||
/>
|
||||
<SpeedDialAction
|
||||
@@ -429,6 +687,13 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput
|
||||
tooltipTitle={"View Stdout/Stderr of Task"}
|
||||
onClick={()=>{setOpenStdoutStderrDialog(true);setOpenSpeedDial(false);}}
|
||||
/>
|
||||
<SpeedDialAction
|
||||
icon={<PlayCircleFilledTwoToneIcon />}
|
||||
arrow
|
||||
tooltipPlacement={"top"}
|
||||
tooltipTitle={"Trigger Eventing Based on Task"}
|
||||
onClick={()=>{onTriggerEventing();setOpenSpeedDial(false);}}
|
||||
/>
|
||||
{task.opsec_pre_blocked === null ? null : ( task.opsec_pre_bypassed === false ? (
|
||||
<SpeedDialAction
|
||||
icon={<LockIcon style={{color: theme.palette.error.main}}/>}
|
||||
|
||||
@@ -38,6 +38,9 @@ const getLoadedCommandsBasedOnInput = ({cmd, ui_feature}) => {
|
||||
id
|
||||
needs_admin
|
||||
payload_type_id
|
||||
payloadtype {
|
||||
name
|
||||
}
|
||||
attributes
|
||||
commandparameters {
|
||||
id
|
||||
@@ -128,7 +131,7 @@ export const TaskFromUIButton = ({callback_id, callback_ids, cmd, ui_feature, pa
|
||||
onTasked({tasked: false});
|
||||
return;
|
||||
}
|
||||
const availableCommands = data.callback_by_pk.loadedcommands.reduce( (prev, cur) => {
|
||||
let availableCommands = data.callback_by_pk.loadedcommands.reduce( (prev, cur) => {
|
||||
if(typeof(parameters) === "string"){
|
||||
return [...prev, {...cur.command, "parsedParameters": {}}];
|
||||
}else{
|
||||
@@ -141,6 +144,7 @@ export const TaskFromUIButton = ({callback_id, callback_ids, cmd, ui_feature, pa
|
||||
return [...prev, {...cur.token, "display": renderValue(cur.token)}]
|
||||
}, []);
|
||||
setCallbackTokenOptions(availableTokens);
|
||||
availableCommands = availableCommands.map(c => {return {...c, display: `${c.cmd} ( ${c.payloadtype.name} )`}});
|
||||
setFileBrowserCommands(availableCommands);
|
||||
if(availableCommands.length === 0){
|
||||
if(ui_feature !== undefined){
|
||||
@@ -226,7 +230,7 @@ export const TaskFromUIButton = ({callback_id, callback_ids, cmd, ui_feature, pa
|
||||
createTask({variables: {...variables, callback_id: callbackData.callback_by_pk.display_id}})
|
||||
}
|
||||
}
|
||||
const submitParametersDialog = (cmd, new_parameters, files) => {
|
||||
const submitParametersDialog = (cmd, new_parameters, files, selectedParameterGroup, payload_type) => {
|
||||
setOpenParametersDialog(false);
|
||||
try{
|
||||
savedFinalVariables.current = JSON.parse(new_parameters);
|
||||
@@ -238,7 +242,15 @@ export const TaskFromUIButton = ({callback_id, callback_ids, cmd, ui_feature, pa
|
||||
savedFinalVariables.current = new_parameters;
|
||||
}
|
||||
|
||||
onSubmitTasking({variables: {callback_id: callbackData.callback_by_pk.display_id, command: cmd, params: new_parameters, files, tasking_location: "modal"}});
|
||||
onSubmitTasking({variables: {
|
||||
callback_id: callbackData.callback_by_pk.display_id,
|
||||
command: cmd,
|
||||
params: new_parameters,
|
||||
files,
|
||||
tasking_location: "modal",
|
||||
payload_type: payload_type,
|
||||
parameter_group_name: selectedParameterGroup
|
||||
}});
|
||||
}
|
||||
const onSubmitSelectedToken = (token) => {
|
||||
setSelectedCallbackToken(token);
|
||||
@@ -304,22 +316,46 @@ export const TaskFromUIButton = ({callback_id, callback_ids, cmd, ui_feature, pa
|
||||
}else{
|
||||
savedFinalVariables.current = parameters;
|
||||
if(typeof(parameters) === "string"){
|
||||
onSubmitTasking({variables: {callback_id: callbackData.callback_by_pk.display_id, command: selectedCommand.cmd, params: parameters, tasking_location: "command_line"}});
|
||||
onSubmitTasking({variables: {
|
||||
callback_id: callbackData.callback_by_pk.display_id,
|
||||
command: selectedCommand.cmd,
|
||||
params: parameters,
|
||||
payload_type: selectedCommand?.payloadtype?.name,
|
||||
tasking_location: "command_line"}});
|
||||
}else{
|
||||
onSubmitTasking({variables: {callback_id: callbackData.callback_by_pk.display_id, command: selectedCommand.cmd, params: JSON.stringify(parameters), tasking_location: taskingLocation}});
|
||||
onSubmitTasking({variables: {
|
||||
callback_id: callbackData.callback_by_pk.display_id,
|
||||
command: selectedCommand.cmd,
|
||||
params: JSON.stringify(parameters),
|
||||
payload_type: selectedCommand?.payloadtype?.name,
|
||||
tasking_location: taskingLocation}});
|
||||
}
|
||||
|
||||
}
|
||||
}else{
|
||||
if(parameters === undefined || parameters === null){
|
||||
savedFinalVariables.current = "";
|
||||
onSubmitTasking({variables: {callback_id: callbackData.callback_by_pk.display_id, command: selectedCommand.cmd, params: ""}});
|
||||
onSubmitTasking({variables: {
|
||||
callback_id: callbackData.callback_by_pk.display_id,
|
||||
command: selectedCommand.cmd,
|
||||
payload_type: selectedCommand?.payloadtype?.name,
|
||||
params: ""}});
|
||||
}else{
|
||||
savedFinalVariables.current = parameters;
|
||||
if(typeof(parameters) === "string"){
|
||||
onSubmitTasking({variables: {callback_id: callbackData.callback_by_pk.display_id, command: selectedCommand.cmd, params: parameters, tasking_location: "command_line"}});
|
||||
onSubmitTasking({variables: {
|
||||
callback_id: callbackData.callback_by_pk.display_id,
|
||||
command: selectedCommand.cmd,
|
||||
payload_type: selectedCommand?.payloadtype?.name,
|
||||
params: parameters,
|
||||
tasking_location: "command_line"}});
|
||||
}else{
|
||||
onSubmitTasking({variables: {callback_id: callbackData.callback_by_pk.display_id, command: selectedCommand.cmd, params: JSON.stringify(parameters), tasking_location: taskingLocation}});
|
||||
onSubmitTasking({variables: {
|
||||
callback_id: callbackData.callback_by_pk.display_id,
|
||||
command: selectedCommand.cmd,
|
||||
payload_type: selectedCommand?.payloadtype?.name,
|
||||
params: JSON.stringify(parameters),
|
||||
tasking_location: taskingLocation}});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,7 +368,7 @@ export const TaskFromUIButton = ({callback_id, callback_ids, cmd, ui_feature, pa
|
||||
onClose={()=>{setOpenSelectCommandDialog(false);onTasked({tasked: false});}}
|
||||
innerDialog={<MythicSelectFromListDialog onClose={()=>{setOpenSelectCommandDialog(false);onTasked({tasked: false});}}
|
||||
onSubmit={onSubmitSelectedCommand} options={fileBrowserCommands} title={"Select Command"}
|
||||
action={"select"} identifier={"id"} display={"cmd"} dontCloseOnSubmit={true} />}
|
||||
action={"select"} identifier={"id"} display={"display"} dontCloseOnSubmit={true} />}
|
||||
/>
|
||||
}
|
||||
{openSelectCallback &&
|
||||
|
||||
@@ -6,6 +6,7 @@ import DialogTitle from '@mui/material/DialogTitle';
|
||||
import MythicTextField from '../../MythicComponents/MythicTextField';
|
||||
import {useQuery, gql, useMutation} from '@apollo/client';
|
||||
import { snackActions } from '../../utilities/Snackbar';
|
||||
import {MythicModifyStringDialog} from "../../MythicComponents/MythicDialog";
|
||||
|
||||
const updateOpsecRequestMutation = gql`
|
||||
mutation requestOpsecBypass ($task_id: Int!) {
|
||||
@@ -83,6 +84,7 @@ export function TaskOpsecDialog(props) {
|
||||
console.error(error);
|
||||
return <div>Error! {error.message}</div>;
|
||||
}
|
||||
const requestAvailable = (opsecData.opsec_pre_blocked === true && !opsecData.opsec_pre_bypassed) || (opsecData.opsec_post_blocked === true && !opsecData.opsec_post_bypassed);
|
||||
const onRequestSubmit = () => {
|
||||
//console.log(props.task_id);
|
||||
updateOpsecRequest({variables: {task_id: props.task_id}});
|
||||
@@ -91,20 +93,15 @@ export function TaskOpsecDialog(props) {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Request OPSEC Bypass</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
<MythicTextField multiline={true} onChange={()=>{}} value={opsecMessage} />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
Close
|
||||
</Button>
|
||||
{(opsecData.opsec_pre_blocked === true && !opsecData.opsec_pre_bypassed) || (opsecData.opsec_post_blocked === true && !opsecData.opsec_post_bypassed) ?
|
||||
(
|
||||
<Button onClick={onRequestSubmit} variant="contained" color="warning">Submit Bypass Request</Button>
|
||||
) : (null) }
|
||||
|
||||
</DialogActions>
|
||||
<MythicModifyStringDialog title={`Request OPSEC Bypass`}
|
||||
onClose={props.onClose}
|
||||
maxRows={40}
|
||||
wrap={true}
|
||||
value={opsecMessage}
|
||||
onSubmit={requestAvailable ? onRequestSubmit : undefined}
|
||||
onSubmitText={"Submit Bypass Request"}
|
||||
dontCloseOnSubmit={true}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {TaskParametersDialogRow} from './TaskParametersDialogRow';
|
||||
import {gql, useLazyQuery, useMutation, useQuery} from '@apollo/client';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
@@ -20,9 +19,9 @@ import Input from '@mui/material/Input';
|
||||
import {UploadTaskFile} from '../../MythicComponents/MythicFileUpload';
|
||||
import {Backdrop, CircularProgress} from '@mui/material';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import {b64DecodeUnicode} from './ResponseDisplay';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import {MythicDraggableDialogTitle} from "../../MythicComponents/MythicDraggableDialogTitle";
|
||||
|
||||
//if we need to get all the loaded commands for the callback and filter, use this
|
||||
const GetLoadedCommandsQuery = gql`
|
||||
@@ -107,7 +106,7 @@ query getAllEdgesQuery($callback_id: Int!){
|
||||
// get all payloads query
|
||||
const getAllPayloadsQuery = gql`
|
||||
query getAllPayloadsQuery($operation_id: Int!){
|
||||
payload(where: {deleted: {_eq: false}, build_phase: {_eq: "success"}, operation_id: {_eq: $operation_id}}) {
|
||||
payload(where: {deleted: {_eq: false}, build_phase: {_eq: "success"}, operation_id: {_eq: $operation_id}}, order_by: {id: desc}) {
|
||||
id
|
||||
description
|
||||
uuid
|
||||
@@ -126,6 +125,7 @@ query getAllPayloadsQuery($operation_id: Int!){
|
||||
filemetum {
|
||||
id
|
||||
filename_text
|
||||
timestamp
|
||||
}
|
||||
buildparameterinstances {
|
||||
value
|
||||
@@ -142,7 +142,7 @@ query getAllPayloadsQuery($operation_id: Int!){
|
||||
// get all payloads on hosts
|
||||
const getAllPayloadsOnHostsQuery = gql`
|
||||
query getAllPayloadsOnHostsQuery($operation_id: Int!){
|
||||
payloadonhost(where: {deleted: {_eq: false}, operation_id: {_eq: $operation_id}, payload: {c2profileparametersinstances: {c2profile: {is_p2p: {_eq: true}}}}}) {
|
||||
payloadonhost(where: {deleted: {_eq: false}, operation_id: {_eq: $operation_id}, payload: {c2profileparametersinstances: {c2profile: {is_p2p: {_eq: true}}}}}, order_by: {id: desc}) {
|
||||
host
|
||||
id
|
||||
payload {
|
||||
@@ -243,6 +243,7 @@ query getCommandQuery($id: Int!){
|
||||
choices
|
||||
choices_are_all_commands
|
||||
choices_are_loaded_commands
|
||||
limit_credentials_by_type
|
||||
default_value
|
||||
description
|
||||
id
|
||||
@@ -256,6 +257,7 @@ query getCommandQuery($id: Int!){
|
||||
parameter_group_name
|
||||
display_name
|
||||
cli_name
|
||||
verifier_regex
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,7 +265,7 @@ query getCommandQuery($id: Int!){
|
||||
// use this to get all the credentials for the command we're trying to execute
|
||||
const getCredentialsQuery = gql`
|
||||
query getCredentialsQuery($operation_id: Int!){
|
||||
credential(where: {deleted: {_eq: false}, operation_id: {_eq: $operation_id}}){
|
||||
credential(where: {deleted: {_eq: false}, operation_id: {_eq: $operation_id}}, order_by: {id: desc}){
|
||||
account
|
||||
comment
|
||||
credential_text
|
||||
@@ -287,7 +289,6 @@ export const commandInParsedParameters = (cmd, parsedParameters) =>{
|
||||
return undefined
|
||||
}
|
||||
export function TaskParametersDialog(props) {
|
||||
const theme = useTheme();
|
||||
const [backdropOpen, setBackdropOpen] = React.useState(false);
|
||||
const [commandInfo, setCommandInfo] = useState({});
|
||||
const [parameterGroups, setParameterGroups] = useState([]);
|
||||
@@ -384,9 +385,8 @@ export function TaskParametersDialog(props) {
|
||||
const addedCredential = (credential) => {
|
||||
getAllCredentials({variables: {operation_id: props.operation_id}});
|
||||
}
|
||||
const intersect = (a, b) => {
|
||||
let setB = new Set(b);
|
||||
return [...new Set(a)].filter(x => setB.has(x));
|
||||
const removedCredential = (credential) => {
|
||||
getAllCredentials({variables: {operation_id: props.operation_id}});
|
||||
}
|
||||
const setSubmenuOpenPreventTasking = (open) => {
|
||||
setSubmenuOpenPreventTask(open);
|
||||
@@ -484,11 +484,17 @@ export function TaskParametersDialog(props) {
|
||||
if(parsedParameterName){
|
||||
return [...prev, {...cmd, value: props.command.parsedParameters[parsedParameterName]}];
|
||||
}else if(cmd.default_value.length > 0){
|
||||
return [...prev, {...cmd, value: JSON.parse(cmd.default_value)}];
|
||||
try {
|
||||
return [...prev, {...cmd, value: JSON.parse(cmd.default_value)}];
|
||||
}catch(error){
|
||||
return [...prev, {...cmd, value: [[cmd.default_value, ""]] }];
|
||||
}
|
||||
|
||||
}else{
|
||||
return [...prev, {...cmd, value: []}];
|
||||
}
|
||||
case "ChooseOne":
|
||||
case "ChooseOneCustom":
|
||||
case "ChooseMultiple":
|
||||
let choices = cmd.choices;
|
||||
let defaultV = cmd.default_value;
|
||||
@@ -511,8 +517,16 @@ export function TaskParametersDialog(props) {
|
||||
let match = true;
|
||||
let cmd_attributes = c.attributes;
|
||||
for(const [key, value] of Object.entries(filter)){
|
||||
if(key === "supported_os" && value.length > 0){
|
||||
if(intersect(value, cmd_attributes[key]).length === 0){
|
||||
if(cmd_attributes[key] === undefined){continue}
|
||||
if(Array.isArray(cmd_attributes[key])){
|
||||
if(key === 'supported_os' && cmd_attributes[key].length === 0){
|
||||
continue;
|
||||
}
|
||||
if(!cmd_attributes[key].includes(value)){
|
||||
match = false;
|
||||
}
|
||||
} else {
|
||||
if(cmd_attributes[key] !== value){
|
||||
match = false;
|
||||
}
|
||||
}
|
||||
@@ -535,8 +549,16 @@ export function TaskParametersDialog(props) {
|
||||
let match = true;
|
||||
let cmd_attributes = c.command.attributes;
|
||||
for(const [key, value] of Object.entries(filter)){
|
||||
if(key === "supported_os" && value.length > 0){
|
||||
if(intersect(value, cmd_attributes[key]).length === 0){
|
||||
if(cmd_attributes[key] === undefined){continue}
|
||||
if(Array.isArray(cmd_attributes[key])){
|
||||
if(key === 'supported_os' && cmd_attributes[key].length === 0){
|
||||
continue;
|
||||
}
|
||||
if(!cmd_attributes[key].includes(value)){
|
||||
match = false;
|
||||
}
|
||||
} else {
|
||||
if(cmd_attributes[key] !== value){
|
||||
match = false;
|
||||
}
|
||||
}
|
||||
@@ -553,21 +575,35 @@ export function TaskParametersDialog(props) {
|
||||
}
|
||||
}
|
||||
if(parsedParameterName){
|
||||
return [...prev, {...cmd, choices: choices, value: props.command.parsedParameters[parsedParameterName]}];
|
||||
return [...prev, {...cmd, choices: choices, value: props.command.parsedParameters[parsedParameterName], default_value: defaultV}];
|
||||
}else{
|
||||
return [...prev, {...cmd, choices: choices, default_value: defaultV, value: defaultV}];
|
||||
}
|
||||
case "File":
|
||||
return [...prev, {...cmd, value: {} }];
|
||||
return [...prev, {...cmd, value: {} }];
|
||||
case "FileMultiple":
|
||||
return [...prev, {...cmd, value: []}];
|
||||
case "CredentialJson":
|
||||
if (loadedCredentialsLoading.credential.length > 0){
|
||||
let credentialChoices = loadedCredentialsLoading.credential;
|
||||
if(credentialChoices === undefined || credentialChoices === null){
|
||||
credentialChoices = [];
|
||||
}
|
||||
if(cmd.limit_credentials_by_type?.length > 0){
|
||||
credentialChoices = credentialChoices.reduce( (existingCreds, curCred) => {
|
||||
if(cmd.limit_credentials_by_type.includes(curCred.type)){
|
||||
return [...existingCreds, curCred];
|
||||
}
|
||||
return [...existingCreds];
|
||||
}, []);
|
||||
}
|
||||
if (credentialChoices.length > 0){
|
||||
if(parsedParameterName){
|
||||
cmd.value = props.command.parsedParameters[parsedParameterName];
|
||||
}
|
||||
else if(cmd.value === "" || (typeof(cmd.value) === Object && Object.keys(cmd.value).length === 0) || cmd.value === undefined){
|
||||
cmd.value = loadedCredentialsLoading.credential[0];
|
||||
cmd.value = credentialChoices[0];
|
||||
}
|
||||
return [...prev, {...cmd, choices: loadedCredentialsLoading.credential}];
|
||||
return [...prev, {...cmd, choices: credentialChoices}];
|
||||
}else{
|
||||
return [...prev, {...cmd, value: {}, choices: []}];
|
||||
}
|
||||
@@ -586,11 +622,7 @@ export function TaskParametersDialog(props) {
|
||||
}
|
||||
|
||||
}, []).sort((a,b) => {
|
||||
if(a.filemetum.filename_text === b.filemetum.filename_text){
|
||||
return a.id < b.id ? 1 : -1
|
||||
}else{
|
||||
return a.filemetum.filename_text < b.filemetum.filename_text ? 1 : -1
|
||||
}
|
||||
return a.id < b.id ? 1 : -1;
|
||||
});
|
||||
|
||||
const callbacksOrganized = loadedAllPayloadsOnHostsLoading.callback.reduce( (prevn, entry) => {
|
||||
@@ -754,11 +786,9 @@ export function TaskParametersDialog(props) {
|
||||
|
||||
}, []);
|
||||
payloads.sort((a,b) => {
|
||||
if(a.filemetum.filename_text === b.filemetum.filename_text){
|
||||
return a.id < b.id ? 1 : -1
|
||||
}else{
|
||||
return a.filemetum.filename_text < b.filemetum.filename_text ? 1 : -1
|
||||
}
|
||||
let aTimestamp = new Date(a.filemetum.timestamp);
|
||||
let bTimestamp = new Date(b.filemetum.timestamp);
|
||||
return aTimestamp < bTimestamp ? 1 : -1;
|
||||
});
|
||||
//now filter the payloads based on supported_agents and supported_agent_build_parameters
|
||||
if(payloads.length > 0){
|
||||
@@ -816,6 +846,7 @@ export function TaskParametersDialog(props) {
|
||||
case "Boolean":
|
||||
case "Number":
|
||||
case "ChooseOne":
|
||||
case "ChooseOneCustom":
|
||||
case "ChooseMultiple":
|
||||
case "PayloadList":
|
||||
case "Array":
|
||||
@@ -832,16 +863,37 @@ export function TaskParametersDialog(props) {
|
||||
collapsedParameters[param.name] = param.value;
|
||||
break
|
||||
case "File":
|
||||
setBackdropOpen(true);
|
||||
const newUUID = await UploadTaskFile(param.value, "Uploaded as part of tasking");
|
||||
if(newUUID){
|
||||
newFileUUIDs.push(newUUID);
|
||||
collapsedParameters[param.name] = newUUID;
|
||||
if(newUUID !== "Missing file in form"){
|
||||
newFileUUIDs.push(newUUID);
|
||||
collapsedParameters[param.name] = newUUID;
|
||||
}
|
||||
}else{
|
||||
setBackdropOpen(false);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "FileMultiple":
|
||||
let fileIDs = [];
|
||||
for(let i = 0; i < param.value.length; i++){
|
||||
if(typeof param.value[i] === "string"){
|
||||
fileIDs.push(param.value[i]);
|
||||
continue
|
||||
}
|
||||
const newUUID = await UploadTaskFile(param.value[i], "Uploaded as part of tasking");
|
||||
if(newUUID){
|
||||
if(newUUID !== "Missing file in form"){
|
||||
newFileUUIDs.push(newUUID);
|
||||
fileIDs.push(newUUID);
|
||||
} else {
|
||||
snackActions.warning("Failed to upload file");
|
||||
}
|
||||
} else {
|
||||
snackActions.warning("Failed to upload file");
|
||||
}
|
||||
}
|
||||
collapsedParameters[param.name] = fileIDs;
|
||||
break;
|
||||
case "CredentialJson":
|
||||
collapsedParameters[param.name] = {
|
||||
account: param.value["account"],
|
||||
@@ -856,7 +908,7 @@ export function TaskParametersDialog(props) {
|
||||
}
|
||||
}
|
||||
setBackdropOpen(false);
|
||||
props.onSubmit(props.command.cmd, JSON.stringify(collapsedParameters), newFileUUIDs, selectedParameterGroup);
|
||||
props.onSubmit(commandInfo.cmd, JSON.stringify(collapsedParameters), newFileUUIDs, selectedParameterGroup, commandInfo?.payloadtype?.name);
|
||||
|
||||
}
|
||||
const onAgentConnectAddNewPayloadOnHost = (host, payload) => {
|
||||
@@ -881,10 +933,52 @@ export function TaskParametersDialog(props) {
|
||||
const onChangeParameterGroup = (event) => {
|
||||
setSelectedParameterGroup(event.target.value);
|
||||
}
|
||||
|
||||
const getOtherParameters = () => {
|
||||
let collapsedParameters = {};
|
||||
for(const param of parameters){
|
||||
switch(param.type){
|
||||
case "String":
|
||||
case "Boolean":
|
||||
case "Number":
|
||||
case "ChooseOne":
|
||||
case "ChooseOneCustom":
|
||||
case "ChooseMultiple":
|
||||
case "PayloadList":
|
||||
case "Array":
|
||||
case "TypedArray":
|
||||
case "LinkInfo":
|
||||
//console.log("submit param", param)
|
||||
collapsedParameters[param.name] = param.value;
|
||||
break;
|
||||
case "AgentConnect":
|
||||
if (Object.keys(param.value).length === 0){
|
||||
snackActions.warning("No connection info specified")
|
||||
return
|
||||
}
|
||||
collapsedParameters[param.name] = param.value;
|
||||
break
|
||||
case "File":
|
||||
|
||||
case "FileMultiple":
|
||||
break
|
||||
case "CredentialJson":
|
||||
collapsedParameters[param.name] = {
|
||||
account: param.value["account"],
|
||||
comment: param.value["comment"],
|
||||
credential: param.value["credential_text"],
|
||||
realm: param.value["realm"],
|
||||
type: param.value["type"]
|
||||
};
|
||||
break;
|
||||
default:
|
||||
console.log("Unknown parameter type");
|
||||
}
|
||||
}
|
||||
return collapsedParameters;
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">{commandInfo.cmd}'s Parameters</DialogTitle>
|
||||
<MythicDraggableDialogTitle>{commandInfo.cmd}'s Parameters</MythicDraggableDialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}}>
|
||||
<CircularProgress color="inherit" />
|
||||
@@ -930,8 +1024,9 @@ export function TaskParametersDialog(props) {
|
||||
callback_id={props.callback_id}
|
||||
onAgentConnectAddNewPayloadOnHost={onAgentConnectAddNewPayloadOnHost}
|
||||
onAgentConnectRemovePayloadOnHost={onAgentConnectRemovePayloadOnHost}
|
||||
addedCredential={addedCredential}
|
||||
addedCredential={addedCredential} removedCredential={removedCredential}
|
||||
setSubmenuOpenPreventTasking={setSubmenuOpenPreventTasking}
|
||||
getOtherParameters={getOtherParameters}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -10,7 +10,7 @@ import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {MythicDraggableDialogTitle} from "../../MythicComponents/MythicDraggableDialogTitle";
|
||||
|
||||
export const allTokenDataFragment = gql`
|
||||
fragment allTokenData on token {
|
||||
@@ -89,11 +89,8 @@ export function TaskTokenDialog(props) {
|
||||
});
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Token Information</DialogTitle>
|
||||
|
||||
|
||||
<Paper elevation={5} style={{position: "relative"}} variant={"elevation"}>
|
||||
<TableContainer component={Paper} className="mythicElement">
|
||||
<MythicDraggableDialogTitle >Token Information</MythicDraggableDialogTitle>
|
||||
<TableContainer className="mythicElement">
|
||||
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
@@ -111,7 +108,6 @@ export function TaskTokenDialog(props) {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Paper>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
Close
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
import React, {useState} from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import {useQuery, gql} from '@apollo/client';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import AceEditor from 'react-ace';
|
||||
import 'ace-builds/src-noconflict/mode-json';
|
||||
import 'ace-builds/src-noconflict/theme-monokai';
|
||||
import 'ace-builds/src-noconflict/theme-xcode';
|
||||
import "ace-builds/src-noconflict/ext-searchbox";
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import {MythicModifyStringDialog} from "../../MythicComponents/MythicDialog";
|
||||
import { meState } from '../../../cache';
|
||||
import {toLocalTime} from "../../utilities/Time";
|
||||
|
||||
const getParametersQuery = gql`
|
||||
query getParametersQuery ($task_id: Int!) {
|
||||
task_by_pk(id: $task_id) {
|
||||
display_params
|
||||
original_params
|
||||
mythic_parsed_params
|
||||
params
|
||||
tasking_location
|
||||
parameter_group_name
|
||||
command_name
|
||||
id
|
||||
timestamp
|
||||
status_timestamp_preprocessing
|
||||
status_timestamp_processing
|
||||
status_timestamp_processed
|
||||
command {
|
||||
cmd
|
||||
id
|
||||
payloadtype {
|
||||
name
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,11 +34,12 @@ query getParametersQuery ($task_id: Int!) {
|
||||
|
||||
export function TaskViewParametersDialog(props) {
|
||||
const [comment, setComment] = useState("");
|
||||
const theme = useTheme();
|
||||
const { loading, error } = useQuery(getParametersQuery, {
|
||||
variables: {task_id: props.task_id},
|
||||
onCompleted: data => {
|
||||
let workingComment = "Original Parameters:\n" + data.task_by_pk.original_params;
|
||||
workingComment += "\n\nMythic Parsed Parameters: \n\tUse this for eventing (create_task->action_data->params_dictionary) and scripting (mythic.issue_task->parameters)\n"
|
||||
workingComment += data.task_by_pk.mythic_parsed_params;
|
||||
workingComment += "\n\nAgent Parameters:\n" + data.task_by_pk.params;
|
||||
workingComment += "\n\nDisplay Parameters:\n" + data.task_by_pk.display_params;
|
||||
workingComment += "\n\nTasking Location:\n" + data.task_by_pk.tasking_location;
|
||||
@@ -46,7 +49,13 @@ export function TaskViewParametersDialog(props) {
|
||||
workingComment += "\n\nOriginal Command: " + data.task_by_pk.command.cmd;
|
||||
workingComment += "\nIssued Command: " + data.task_by_pk.command_name;
|
||||
}
|
||||
workingComment += "\n\nPayload Type:\n" + data.task_by_pk.command.payloadtype.name;
|
||||
}
|
||||
workingComment += "\n\n--------IMPORTANT TIMESTAMPS--------\n\n";
|
||||
workingComment += "Task Submitted by Operator : " + toLocalTime(data.task_by_pk.status_timestamp_preprocessing, meState()?.user?.view_utc_time) + "\n";
|
||||
workingComment += "Task Picked up by Agent : " + toLocalTime(data.task_by_pk.status_timestamp_processing, meState()?.user?.view_utc_time) + "\n";
|
||||
workingComment += "First message from Task : " + toLocalTime(data.task_by_pk.status_timestamp_processed, meState()?.user?.view_utc_time) + "\n";
|
||||
workingComment += "Last message from Task : " + toLocalTime(data.task_by_pk.timestamp, meState()?.user?.view_utc_time) + "\n";
|
||||
setComment(workingComment);
|
||||
},
|
||||
fetchPolicy: "network-only"
|
||||
@@ -59,30 +68,14 @@ export function TaskViewParametersDialog(props) {
|
||||
return <div>Error!</div>;
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">View Task Parameters</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
|
||||
fontSize={14}
|
||||
showGutter={true}
|
||||
highlightActiveLine={true}
|
||||
value={comment}
|
||||
focus={true}
|
||||
width={"100%"}
|
||||
setOptions={{
|
||||
showLineNumbers: true,
|
||||
useWorker: false,
|
||||
tabSize: 4
|
||||
}}/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</React.Fragment>
|
||||
<>
|
||||
<MythicModifyStringDialog title={`View Task Parameters And Timestamps`}
|
||||
onClose={props.onClose}
|
||||
maxRows={40}
|
||||
wrap={true}
|
||||
value={comment} />
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import React, {useState} from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import {useQuery, gql} from '@apollo/client';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import AceEditor from 'react-ace';
|
||||
import 'ace-builds/src-noconflict/mode-json';
|
||||
import 'ace-builds/src-noconflict/theme-monokai';
|
||||
import 'ace-builds/src-noconflict/theme-xcode';
|
||||
import "ace-builds/src-noconflict/ext-searchbox";
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import {MythicModifyStringDialog} from "../../MythicComponents/MythicDialog";
|
||||
|
||||
const getParametersQuery = gql`
|
||||
query getParametersQuery ($task_id: Int!) {
|
||||
@@ -24,7 +16,6 @@ query getParametersQuery ($task_id: Int!) {
|
||||
|
||||
export function TaskViewStdoutStderrDialog(props) {
|
||||
const [comment, setComment] = useState("");
|
||||
const theme = useTheme();
|
||||
const { loading, error } = useQuery(getParametersQuery, {
|
||||
variables: {task_id: props.task_id},
|
||||
onCompleted: data => {
|
||||
@@ -41,28 +32,11 @@ export function TaskViewStdoutStderrDialog(props) {
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">View Task Stdout/Stderr</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
|
||||
fontSize={14}
|
||||
showGutter={true}
|
||||
highlightActiveLine={true}
|
||||
value={comment}
|
||||
focus={true}
|
||||
width={"100%"}
|
||||
setOptions={{
|
||||
showLineNumbers: true,
|
||||
tabSize: 4,
|
||||
useWorker: false,
|
||||
}}/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
<MythicModifyStringDialog title={`View Task Stdout/Stderr`}
|
||||
onClose={props.onClose}
|
||||
maxRows={40}
|
||||
wrap={true}
|
||||
value={comment} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
Loaded 100 of 660 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user